Merge branch 'develop' into mention-text-and-pills-ux

This commit is contained in:
taffyko 2024-11-15 14:43:30 -08:00
commit 3702cc03d7
506 changed files with 6225 additions and 7447 deletions

View file

@ -496,6 +496,8 @@ describe("DecryptionFailureTracker", function () {
await createAndTrackEventWithError(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_KEY_WITHHELD);
await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE);
await createAndTrackEventWithError(DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED);
await createAndTrackEventWithError(DecryptionFailureCode.UNSIGNED_SENDER_DEVICE);
await createAndTrackEventWithError(DecryptionFailureCode.UNKNOWN_ERROR);
// Pretend "now" is Infinity
@ -510,6 +512,8 @@ describe("DecryptionFailureTracker", function () {
"ExpectedDueToMembership",
"OlmKeysNotSentError",
"RoomKeysWithheldForUnverifiedDevice",
"ExpectedVerificationViolation",
"ExpectedSentByInsecureDevice",
"UnknownError",
]);
});

View file

@ -39,6 +39,7 @@ jest.mock("matrix-js-sdk/src/logger");
jest.mock("../../src/dispatcher/dispatcher", () => ({
dispatch: jest.fn(),
register: jest.fn(),
unregister: jest.fn(),
}));
jest.mock("../../src/SecurityManager", () => ({
@ -351,13 +352,13 @@ describe("DeviceListener", () => {
mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc");
});
it("shows upgrade encryption toast when user has a key backup available", async () => {
it("shows set up encryption toast when user has a key backup available", async () => {
// non falsy response
mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo);
await createAndStart();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION,
SetupEncryptionToast.Kind.SET_UP_ENCRYPTION,
);
});
});

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

@ -28,6 +28,12 @@ const checkTabIndexes = (buttons: NodeListOf<HTMLElement>, expectations: number[
expect([...buttons].map((b) => b.tabIndex)).toStrictEqual(expectations);
};
const createButtonElement = (text: string): HTMLButtonElement => {
const button = document.createElement("button");
button.textContent = text;
return button;
};
// give the buttons keys for the fibre reconciler to not treat them all as the same
const button1 = <Button key={1}>a</Button>;
const button2 = <Button key={2}>b</Button>;
@ -114,6 +120,25 @@ describe("RovingTabIndex", () => {
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
it("RovingTabIndexProvider provides a ref to the dom element", () => {
const nodeRef = React.createRef<HTMLButtonElement>();
const MyButton = (props: HTMLAttributes<HTMLButtonElement>) => {
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLButtonElement>(nodeRef);
return <button {...props} onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref} />;
};
const { container } = render(
<RovingTabIndexProvider>
{() => (
<React.Fragment>
<MyButton />
</React.Fragment>
)}
</RovingTabIndexProvider>,
);
// nodeRef should point to button
expect(nodeRef.current).toBe(container.querySelector("button"));
});
it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => {
const { container } = render(
<RovingTabIndexProvider>
@ -123,11 +148,7 @@ describe("RovingTabIndex", () => {
{button2}
<RovingTabIndexWrapper>
{({ onFocus, isActive, ref }) => (
<button
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
ref={ref as React.RefObject<HTMLButtonElement>}
>
<button onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref}>
.
</button>
)}
@ -147,75 +168,75 @@ describe("RovingTabIndex", () => {
describe("reducer functions as expected", () => {
it("SetFocus works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
const node1 = createButtonElement("Button 1");
const node2 = createButtonElement("Button 2");
expect(
reducer(
{
activeRef: ref1,
refs: [ref1, ref2],
activeNode: node1,
nodes: [node1, node2],
},
{
type: Type.SetFocus,
payload: {
ref: ref2,
node: node2,
},
},
),
).toStrictEqual({
activeRef: ref2,
refs: [ref1, ref2],
activeNode: node2,
nodes: [node1, node2],
});
});
it("Unregister works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
const ref3 = React.createRef<HTMLElement>();
const ref4 = React.createRef<HTMLElement>();
const button1 = createButtonElement("Button 1");
const button2 = createButtonElement("Button 2");
const button3 = createButtonElement("Button 3");
const button4 = createButtonElement("Button 4");
let state: IState = {
refs: [ref1, ref2, ref3, ref4],
nodes: [button1, button2, button3, button4],
};
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref2,
node: button2,
},
});
expect(state).toStrictEqual({
refs: [ref1, ref3, ref4],
nodes: [button1, button3, button4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref3,
node: button3,
},
});
expect(state).toStrictEqual({
refs: [ref1, ref4],
nodes: [button1, button4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref4,
node: button4,
},
});
expect(state).toStrictEqual({
refs: [ref1],
nodes: [button1],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref1,
node: button1,
},
});
expect(state).toStrictEqual({
refs: [],
nodes: [],
});
});
@ -235,122 +256,122 @@ describe("RovingTabIndex", () => {
);
let state: IState = {
refs: [],
nodes: [],
};
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref1,
node: ref1.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1],
activeNode: ref1.current,
nodes: [ref1.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2],
activeNode: ref1.current,
nodes: [ref1.current, ref2.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref3,
node: ref3.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2, ref3],
activeNode: ref1.current,
nodes: [ref1.current, ref2.current, ref3.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref4,
node: ref4.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref1.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
// test that the automatic focus switch works for unmounting
state = reducer(state, {
type: Type.SetFocus,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref2,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref2.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref3, ref4],
activeNode: ref3.current,
nodes: [ref1.current, ref3.current, ref4.current],
});
// test that the insert into the middle works as expected
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref3.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
// test that insertion at the edges works
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref1,
node: ref1.current!,
},
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref4,
node: ref4.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref2, ref3],
activeNode: ref3.current,
nodes: [ref2.current, ref3.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref1,
node: ref1.current!,
},
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref4,
node: ref4.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref3.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
});
});

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

@ -62,6 +62,7 @@ import { DRAFT_LAST_CLEANUP_KEY } from "../../../../src/DraftCleaner";
import { UIFeature } from "../../../../src/settings/UIFeature";
import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils";
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
import Modal from "../../../../src/Modal.tsx";
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
completeAuthorizationCodeGrant: jest.fn(),
@ -148,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 = {
@ -952,7 +954,7 @@ describe("<MatrixChat />", () => {
const getComponentAndWaitForReady = async (): Promise<RenderResult> => {
const renderResult = getComponent();
// wait for welcome page chrome render
await screen.findByText("powered by Matrix");
await screen.findByText("Powered by Matrix");
// go to login page
defaultDispatcher.dispatch({
@ -1479,7 +1481,7 @@ describe("<MatrixChat />", () => {
const getComponentAndWaitForReady = async (): Promise<RenderResult> => {
const renderResult = getComponent();
// wait for welcome page chrome render
await screen.findByText("powered by Matrix");
await screen.findByText("Powered by Matrix");
// go to mobile_register page
defaultDispatcher.dispatch({
@ -1499,7 +1501,7 @@ describe("<MatrixChat />", () => {
it("should render welcome screen if mobile registration is not enabled in settings", async () => {
await getComponentAndWaitForReady();
await screen.findByText("powered by Matrix");
await screen.findByText("Powered by Matrix");
});
it("should render mobile registration", async () => {
@ -1514,7 +1516,9 @@ describe("<MatrixChat />", () => {
describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => {
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
@ -1526,7 +1530,26 @@ describe("<MatrixChat />", () => {
});
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(screen.getByText("mocked dialog")).toBeInTheDocument());
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
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

@ -114,46 +114,56 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
>
<div
class="mx_AuthPage_modal"
style="position: relative;"
>
<div
class="mx_Welcome"
data-testid="mx_welcome_screen"
class="mx_AuthPage_modalBlur"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; filter: blur(40px);"
/>
<div
class="mx_AuthPage_modalContent"
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
>
<div
class="mx_WelcomePage mx_WelcomePage_loggedIn"
class="mx_Welcome"
data-testid="mx_welcome_screen"
>
<div
class="mx_WelcomePage_body"
>
<h1>
Hello
</h1>
</div>
</div>
<div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
class="mx_WelcomePage mx_WelcomePage_loggedIn"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
class="mx_WelcomePage_body"
>
<div>
English
</div>
<h1>
Hello
</h1>
</div>
</div>
<div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
@ -162,12 +172,33 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
class="mx_AuthFooter"
role="contentinfo"
>
<a
href="https://element.io/blog"
rel="noreferrer noopener"
target="_blank"
>
Blog
</a>
<a
href="https://twitter.com/element_hq"
rel="noreferrer noopener"
target="_blank"
>
Twitter
</a>
<a
href="https://github.com/element-hq/element-web"
rel="noreferrer noopener"
target="_blank"
>
GitHub
</a>
<a
href="https://matrix.org"
rel="noreferrer noopener"
target="_blank"
>
powered by Matrix
Powered by Matrix
</a>
</footer>
</div>
@ -201,116 +232,150 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo
>
<div
class="mx_AuthPage_modal"
style="position: relative;"
>
<div
class="mx_AuthHeader"
class="mx_AuthPage_modalBlur"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; filter: blur(40px);"
/>
<div
class="mx_AuthPage_modalContent"
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
>
<aside
class="mx_AuthHeaderLogo"
>
Matrix
</aside>
<div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
class="mx_AuthHeader"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
<aside
class="mx_AuthHeaderLogo"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
<img
alt="Element"
src="themes/element/img/logos/element-logo.svg"
/>
</div>
</div>
</div>
<main
class="mx_AuthBody"
>
<h1>
You're signed out
</h1>
<h2>
Sign in
</h2>
<div>
<form>
<p>
Enter your password to sign in and regain access to your account.
</p>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Password
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Sign in
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Forgotten your password?
</div>
</form>
</div>
<h2>
Clear personal data
</h2>
<p>
Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.
</p>
<div>
</aside>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
Clear all data
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
</main>
<main
class="mx_AuthBody"
>
<h1>
You're signed out
</h1>
<h2>
Sign in
</h2>
<div>
<form>
<p>
Enter your password to sign in and regain access to your account.
</p>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Password
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Sign in
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Forgotten your password?
</div>
</form>
</div>
<h2>
Clear personal data
</h2>
<p>
Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.
</p>
<div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Clear all data
</div>
</div>
</main>
</div>
</div>
<footer
class="mx_AuthFooter"
role="contentinfo"
>
<a
href="https://element.io/blog"
rel="noreferrer noopener"
target="_blank"
>
Blog
</a>
<a
href="https://twitter.com/element_hq"
rel="noreferrer noopener"
target="_blank"
>
Twitter
</a>
<a
href="https://github.com/element-hq/element-web"
rel="noreferrer noopener"
target="_blank"
>
GitHub
</a>
<a
href="https://matrix.org"
rel="noreferrer noopener"
target="_blank"
>
powered by Matrix
Powered by Matrix
</a>
</footer>
</div>

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { mocked } from "jest-mock";
import { act, render, RenderResult, screen } from "jest-matrix-react";
import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
@ -47,14 +47,12 @@ describe("<ForgotPassword>", () => {
};
const click = async (element: Element): Promise<void> => {
await act(async () => {
await userEvent.click(element, { delay: null });
});
await userEvent.click(element, { delay: null });
};
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
it("should close the dialog and show the password input", () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
it("should close the dialog and show the password input", async () => {
await waitFor(() => expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument());
expect(screen.getByText("Reset your password")).toBeInTheDocument();
});
};
@ -314,7 +312,7 @@ describe("<ForgotPassword>", () => {
});
});
it("should send the new password and show the click validation link dialog", () => {
it("should send the new password and show the click validation link dialog", async () => {
expect(client.setPassword).toHaveBeenCalledWith(
{
type: "m.login.email.identity",
@ -326,15 +324,15 @@ describe("<ForgotPassword>", () => {
testPassword,
false,
);
expect(screen.getByText("Verify your email to continue")).toBeInTheDocument();
await expect(
screen.findByText("Verify your email to continue"),
).resolves.toBeInTheDocument();
expect(screen.getByText(testEmail)).toBeInTheDocument();
});
describe("and dismissing the dialog by clicking the background", () => {
beforeEach(async () => {
await act(async () => {
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
});
await userEvent.click(await screen.findByTestId("dialog-background"), { delay: null });
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
@ -345,7 +343,7 @@ describe("<ForgotPassword>", () => {
describe("and dismissing the dialog", () => {
beforeEach(async () => {
await click(screen.getByLabelText("Close dialog"));
await click(await screen.findByLabelText("Close dialog"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
@ -356,14 +354,16 @@ describe("<ForgotPassword>", () => {
describe("and clicking »Re-enter email address«", () => {
beforeEach(async () => {
await click(screen.getByText("Re-enter email address"));
await click(await screen.findByText("Re-enter email address"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
});
it("should close the dialog and go back to the email input", () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
it("should close the dialog and go back to the email input", async () => {
await waitFor(() =>
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(),
);
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
});
});
@ -397,11 +397,11 @@ describe("<ForgotPassword>", () => {
});
it("should show the sign out warning dialog", async () => {
expect(
screen.getByText(
await expect(
screen.findByText(
"Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
),
).toBeInTheDocument();
).resolves.toBeInTheDocument();
// confirm dialog
await click(screen.getByText("Continue"));

View file

@ -9,16 +9,16 @@ Please see LICENSE files in the repository root for full details.
import * as React from "react";
import { render } from "jest-matrix-react";
import VectorAuthPage from "../../../../../src/components/views/auth/VectorAuthPage";
import AuthFooter from "../../../../../src/components/views/auth/AuthFooter";
import { setupLanguageMock } from "../../../../setup/setupLanguage";
describe("<VectorAuthPage />", () => {
describe("<AuthFooter />", () => {
beforeEach(() => {
setupLanguageMock();
});
it("should match snapshot", () => {
const { asFragment } = render(<VectorAuthPage />);
const { asFragment } = render(<AuthFooter />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details.
import * as React from "react";
import { render } from "jest-matrix-react";
import VectorAuthHeaderLogo from "../../../../../src/components/views/auth/VectorAuthHeaderLogo";
import AuthHeaderLogo from "../../../../../src/components/views/auth/AuthHeaderLogo";
describe("<VectorAuthHeaderLogo />", () => {
describe("<AuthHeaderLogo />", () => {
it("should match snapshot", () => {
const { asFragment } = render(<VectorAuthHeaderLogo />);
const { asFragment } = render(<AuthHeaderLogo />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,36 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import { render } from "jest-matrix-react";
import AuthPage from "../../../../../src/components/views/auth/AuthPage";
import { setupLanguageMock } from "../../../../setup/setupLanguage";
import SdkConfig from "../../../../../src/SdkConfig.ts";
describe("<AuthPage />", () => {
beforeEach(() => {
setupLanguageMock();
SdkConfig.reset();
// @ts-ignore private access
AuthPage.welcomeBackgroundUrl = undefined;
});
it("should match snapshot", () => {
const { asFragment } = render(<AuthPage />);
expect(asFragment()).toMatchSnapshot();
});
it("should use configured background url", () => {
SdkConfig.add({ branding: { welcome_background_url: ["https://example.com/image.png"] } });
const { container } = render(<AuthPage />);
expect(container.querySelector(".mx_AuthPage")).toHaveStyle({
background: "center/cover fixed url(https://example.com/image.png)",
});
});
});

View file

@ -1,24 +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
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import { render } from "jest-matrix-react";
import VectorAuthFooter from "../../../../../src/components/views/auth/VectorAuthFooter";
import { setupLanguageMock } from "../../../../setup/setupLanguage";
describe("<VectorAuthFooter />", () => {
beforeEach(() => {
setupLanguageMock();
});
it("should match snapshot", () => {
const { asFragment } = render(<VectorAuthFooter />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VectorAuthFooter /> should match snapshot 1`] = `
exports[`<AuthFooter /> should match snapshot 1`] = `
<DocumentFragment>
<footer
class="mx_AuthFooter"

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VectorAuthHeaderLogo /> should match snapshot 1`] = `
exports[`<AuthHeaderLogo /> should match snapshot 1`] = `
<DocumentFragment>
<aside
class="mx_AuthHeaderLogo"

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VectorAuthPage /> should match snapshot 1`] = `
exports[`<AuthPage /> should match snapshot 1`] = `
<DocumentFragment>
<div
class="mx_AuthPage"

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { screen } from "jest-matrix-react";
import { screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { flushPromises, mkEvent, stubClient } from "../../../../test-utils";
@ -31,12 +31,12 @@ describe("ConfirmRedactDialog", () => {
};
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
createRedactEventDialog({ mxEvent });
act(() => createRedactEventDialog({ mxEvent }));
// double-flush promises required for the dialog to show up
await flushPromises();
await flushPromises();
await userEvent.click(screen.getByTestId("dialog-primary-button"));
await userEvent.click(await screen.findByTestId("dialog-primary-button"));
};
beforeEach(() => {

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

@ -10,42 +10,23 @@ import { render, RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { mocked, MockedObject } from "jest-mock";
import { Crypto, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import {
filterConsole,
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsServer,
} from "../../../../../test-utils";
import { filterConsole, stubClient } from "../../../../../test-utils";
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "../../../../../../src/Modal";
import RestoreKeyBackupDialog from "../../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog";
describe("CreateSecretStorageDialog", () => {
let mockClient: MockedObject<MatrixClient>;
let mockCrypto: MockedObject<Crypto.CryptoApi>;
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsServer(),
...mockClientMethodsCrypto(),
uploadDeviceSigningKeys: jest.fn().mockImplementation(async () => {
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
throw new MatrixError({ flows: [] });
}),
});
mockCrypto = mocked(mockClient.getCrypto()!);
Object.assign(mockCrypto, {
isKeyBackupTrusted: jest.fn(),
isDehydrationSupported: jest.fn(() => false),
bootstrapCrossSigning: jest.fn(),
bootstrapSecretStorage: jest.fn(),
mockClient = mocked(stubClient());
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
throw new MatrixError({ flows: [] });
});
// Mock the clipboard API
document.execCommand = jest.fn().mockReturnValue(true);
});
afterEach(() => {
@ -59,11 +40,37 @@ describe("CreateSecretStorageDialog", () => {
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
}
it("shows a loading spinner initially", async () => {
const { container } = renderComponent();
expect(screen.getByTestId("spinner")).toBeDefined();
expect(container).toMatchSnapshot();
await flushPromises();
it("handles the happy path", async () => {
const result = renderComponent();
await result.findByText(
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
);
expect(result.container).toMatchSnapshot();
await userEvent.click(result.getByRole("button", { name: "Continue" }));
await screen.findByText("Save your Security Key");
expect(result.container).toMatchSnapshot();
// Copy the key to enable the continue button
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
expect(result.queryByText("Copied!")).not.toBeNull();
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
await screen.findByText("Your keys are now being backed up from this device.");
});
it("when there is an error when bootstraping the secret storage, it shows an error", async () => {
jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockRejectedValue(new Error("error"));
renderComponent();
await screen.findByText(
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
);
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
await screen.findByText("Save your Security Key");
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
await screen.findByText("Unable to set up secret storage");
});
describe("when there is an error fetching the backup version", () => {
@ -75,139 +82,19 @@ describe("CreateSecretStorageDialog", () => {
});
const result = renderComponent();
// We go though the dialog until we have to get the key backup
await userEvent.click(result.getByRole("button", { name: "Continue" }));
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
// XXX the error message is... misleading.
await result.findByText("Unable to query secret storage status");
expect(result.container).toMatchSnapshot();
});
});
it("shows 'Generate a Security Key' text if no key backup is present", async () => {
const result = renderComponent();
await flushPromises();
expect(result.container).toMatchSnapshot();
result.getByText("Generate a Security Key");
});
describe("when canUploadKeysWithPasswordOnly", () => {
// spy on Modal.createDialog
let modalSpy: jest.SpyInstance;
// deferred which should be resolved to indicate that the created dialog has completed
let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>;
beforeEach(() => {
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
await sleep(0);
throw new MatrixError({
flows: [{ stages: ["m.login.password"] }],
});
});
restoreDialogFinishedDefer = defer<[done?: boolean]>();
modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: restoreDialogFinishedDefer.promise,
close: jest.fn(),
});
});
it("prompts for a password and then shows RestoreKeyBackupDialog", async () => {
const result = renderComponent();
await result.findByText(/Enter your account password to confirm the upgrade/);
await screen.findByText("Unable to query secret storage status");
expect(result.container).toMatchSnapshot();
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
result.getByRole("button", { name: "Next" }).click();
expect(modalSpy).toHaveBeenCalledWith(
RestoreKeyBackupDialog,
{
showSummary: false,
},
undefined,
false,
false,
);
restoreDialogFinishedDefer.resolve([]);
});
it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => {
const result = renderComponent();
await result.findByText(/Enter your account password to confirm the upgrade/);
expect(result.container).toMatchSnapshot();
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
result.getByRole("button", { name: "Next" }).click();
expect(modalSpy).toHaveBeenCalled();
// While we restore the key backup, its signature becomes accepted
mockCrypto.isKeyBackupTrusted.mockResolvedValue({ trusted: true } as BackupTrustInfo);
restoreDialogFinishedDefer.resolve([]);
await flushPromises();
// XXX no idea why this is a sensible thing to do. I just work here.
expect(mockCrypto.bootstrapCrossSigning).toHaveBeenCalled();
expect(mockCrypto.bootstrapSecretStorage).toHaveBeenCalled();
await result.findByText("Your keys are now being backed up from this device.");
});
describe("when there is an error fetching the backup version after RestoreKeyBackupDialog", () => {
filterConsole("Error fetching backup data from server");
it("handles the error sensibly", async () => {
const result = renderComponent();
await result.findByText(/Enter your account password to confirm the upgrade/);
expect(result.container).toMatchSnapshot();
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
result.getByRole("button", { name: "Next" }).click();
expect(modalSpy).toHaveBeenCalled();
mockClient.getKeyBackupVersion.mockImplementation(async () => {
throw new Error("bleh bleh");
});
restoreDialogFinishedDefer.resolve([]);
await result.findByText("Unable to query secret storage status");
});
});
});
describe("when backup is present but not trusted", () => {
beforeEach(() => {
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
});
it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => {
const result = renderComponent();
await result.findByText("Restore your key backup to upgrade your encryption");
expect(result.container).toMatchSnapshot();
// before we click "Restore", set up a spy on createDialog
const restoreDialogFinishedDefer = defer<[done?: boolean]>();
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: restoreDialogFinishedDefer.promise,
close: jest.fn(),
});
result.getByRole("button", { name: "Restore" }).click();
expect(modalSpy).toHaveBeenCalledWith(
RestoreKeyBackupDialog,
{
showSummary: false,
},
undefined,
false,
false,
);
// simulate RestoreKeyBackupDialog completing, to run that code path
restoreDialogFinishedDefer.resolve([]);
// Now we can get the backup and we retry
mockClient.getKeyBackupVersion.mockRestore();
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
await screen.findByText("Your keys are now being backed up from this device.");
});
});
});

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

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = `
exports[`CreateSecretStorageDialog handles the happy path 1`] = `
<div>
<div
data-focus-guard="true"
@ -128,7 +128,7 @@ exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no ke
</div>
`;
exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
exports[`CreateSecretStorageDialog handles the happy path 2`] = `
<div>
<div
data-focus-guard="true"
@ -143,19 +143,68 @@ exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
>
<div
class="mx_Dialog_header"
/>
>
<h1
class="mx_Heading_h3 mx_Dialog_title mx_CreateSecretStorageDialog_titleWithIcon mx_CreateSecretStorageDialog_secureBackupTitle"
id="mx_BaseDialog_title"
>
Save your Security Key
</h1>
</div>
<div>
<div>
<p>
Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.
</p>
<div
class="mx_Spinner"
class="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
class="mx_CreateSecretStorageDialog_recoveryKeyContainer"
>
<div
class="mx_CreateSecretStorageDialog_recoveryKey"
>
<code />
</div>
<div
class="mx_CreateSecretStorageDialog_recoveryKeyButtons"
>
<div
class="mx_AccessibleButton mx_Dialog_primary mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Download
</div>
<span>
or
</span>
<div
class="mx_AccessibleButton mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Copy
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Continue
</button>
</span>
</div>
</div>
</div>
@ -168,331 +217,6 @@ exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
</div>
`;
exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = `
<div>
<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_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Restore your key backup to upgrade your encryption
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Restore
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = `
<div>
<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_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Enter your account password to confirm the upgrade:
</div>
<div>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_CreateSecretStorageDialog_password"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_CreateSecretStorageDialog_password"
>
Password
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = `
<div>
<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_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Enter your account password to confirm the upgrade:
</div>
<div>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_CreateSecretStorageDialog_password"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_CreateSecretStorageDialog_password"
>
Password
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = `
<div>
<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_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Enter your account password to confirm the upgrade:
</div>
<div>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_CreateSecretStorageDialog_password"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_CreateSecretStorageDialog_password"
>
Password
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when there is an error fetching the backup version shows an error 1`] = `
<div>
<div

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

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { mocked } from "jest-mock";
import { fireEvent, render, screen } from "jest-matrix-react";
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
import { TimestampToEventResponse, ConnectionError, HTTPError, MatrixError } from "matrix-js-sdk/src/matrix";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
@ -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(
@ -291,7 +297,9 @@ describe("DateSeparator", () => {
// The submit debug logs option should *NOT* be shown for network errors.
//
// We have to use `queryBy` so that it can return `null` for something that does not exist.
expect(screen.queryByTestId("jump-to-date-error-submit-debug-logs-button")).not.toBeInTheDocument();
await waitFor(() =>
expect(screen.queryByTestId("jump-to-date-error-submit-debug-logs-button")).not.toBeInTheDocument(),
);
});
});
});

View file

@ -129,6 +129,6 @@ describe("DecryptionFailureBody", () => {
const { container } = customRender(event);
// Then
expect(container).toHaveTextContent("Encrypted by a device not verified by its owner");
expect(container).toHaveTextContent("Sent from an insecure device");
});
});

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

@ -23,7 +23,7 @@ exports[`DecryptionFailureBody Should display "Unable to decrypt message" 1`] =
exports[`DecryptionFailureBody should handle messages from users who change identities after verification 1`] = `
<div>
<div
class="mx_DecryptionFailureBody mx_EventTile_content mx_DecryptionFailureVerifiedIdentityChanged"
class="mx_DecryptionFailureBody mx_EventTile_content mx_DecryptionFailureSenderTrustRequirement"
>
<span>
<svg
@ -35,15 +35,10 @@ exports[`DecryptionFailureBody should handle messages from users who change iden
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.713 17.713A.968.968 0 0 1 12 18a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 17a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 16a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 17a.97.97 0 0 1-.287.713Zm0-4A.968.968 0 0 1 12 14a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 13V9a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 8a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 9v4a.97.97 0 0 1-.287.713Z"
/>
<path
clip-rule="evenodd"
d="M10.264 3.039c.767-1.344 2.705-1.344 3.472 0l8.554 14.969c.762 1.333-.2 2.992-1.736 2.992H3.446c-1.535 0-2.498-1.659-1.736-2.992l8.553-14.969ZM3.446 19 12 4.031l8.554 14.97H3.446Z"
fill-rule="evenodd"
d="M12 22a9.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 22Zm0-2c2.233 0 4.125-.775 5.675-2.325C19.225 16.125 20 14.233 20 12c0-.9-.146-1.767-.438-2.6A7.951 7.951 0 0 0 18.3 7.1L7.1 18.3c.7.55 1.467.97 2.3 1.262.833.292 1.7.438 2.6.438Zm-6.3-3.1L16.9 5.7a7.95 7.95 0 0 0-2.3-1.263A7.813 7.813 0 0 0 12 4c-2.233 0-4.125.775-5.675 2.325C4.775 7.875 4 9.767 4 12c0 .9.146 1.767.438 2.6A7.95 7.95 0 0 0 5.7 16.9Z"
/>
</svg>
Verified identity has changed
Sender's verified identity has changed
</span>
</div>
</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

@ -19,7 +19,13 @@ import {
Room,
TweakName,
} from "matrix-js-sdk/src/matrix";
import { CryptoApi, EventEncryptionInfo, EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api";
import {
CryptoApi,
DecryptionFailureCode,
EventEncryptionInfo,
EventShieldColour,
EventShieldReason,
} from "matrix-js-sdk/src/crypto-api";
import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
import EventTile, { EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
@ -350,6 +356,32 @@ describe("EventTile", () => {
"mx_EventTile_e2eIcon_decryption_failure",
);
});
it("should not show a shield for previously-verified users", async () => {
mxEvent = mkEvent({
type: "m.room.encrypted",
room: room.roomId,
user: "@alice:example.org",
event: true,
content: {},
});
const mockCrypto = {
decryptEvent: async (_ev): Promise<IEventDecryptionResult> => {
throw new Error("can't decrypt");
},
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
await mxEvent.attemptDecryption(mockCrypto);
mxEvent["_decryptionFailureReason"] = DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED;
const { container } = getComponent();
await act(flushPromises);
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
});
});
it("should update the warning when the event is edited", async () => {

View file

@ -14,7 +14,7 @@ import userEvent from "@testing-library/user-event";
import * as pinnedEventHooks from "../../../../../src/hooks/usePinnedEvents";
import { PinnedMessageBanner } from "../../../../../src/components/views/rooms/PinnedMessageBanner";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { makePollStartEvent, stubClient } from "../../../../test-utils";
import { makePollStartEvent, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import dis from "../../../../../src/dispatcher/dispatcher";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
@ -76,7 +76,10 @@ describe("<PinnedMessageBanner />", () => {
* Render the banner
*/
function renderBanner() {
return render(<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />);
return render(
<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />,
withClientContextRenderOptions(mockClient),
);
}
it("should render nothing when there are no pinned events", async () => {
@ -92,7 +95,7 @@ describe("<PinnedMessageBanner />", () => {
const { asFragment } = renderBanner();
expect(screen.getByText("First pinned message")).toBeVisible();
await expect(screen.findByText("First pinned message")).resolves.toBeVisible();
expect(screen.queryByRole("button", { name: "View all" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
@ -103,7 +106,7 @@ describe("<PinnedMessageBanner />", () => {
const { asFragment } = renderBanner();
expect(screen.getByText("Second pinned message")).toBeVisible();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
expect(screen.getByTestId("banner-counter")).toHaveTextContent("2 of 2 Pinned messages");
expect(screen.getAllByTestId("banner-indicator")).toHaveLength(2);
expect(screen.queryByRole("button", { name: "View all" })).toBeVisible();
@ -121,7 +124,7 @@ describe("<PinnedMessageBanner />", () => {
const { asFragment } = renderBanner();
expect(screen.getByText("Fourth pinned message")).toBeVisible();
await expect(screen.findByText("Fourth pinned message")).resolves.toBeVisible();
expect(screen.getByTestId("banner-counter")).toHaveTextContent("4 of 4 Pinned messages");
expect(screen.getAllByTestId("banner-indicator")).toHaveLength(3);
expect(screen.queryByRole("button", { name: "View all" })).toBeVisible();
@ -143,7 +146,7 @@ describe("<PinnedMessageBanner />", () => {
]);
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2, event3]);
rerender(<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />);
expect(screen.getByText("Third pinned message")).toBeVisible();
await expect(screen.findByText("Third pinned message")).resolves.toBeVisible();
expect(asFragment()).toMatchSnapshot();
});
@ -152,7 +155,7 @@ describe("<PinnedMessageBanner />", () => {
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2]);
renderBanner();
expect(screen.getByText("Second pinned message")).toBeVisible();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
await userEvent.click(screen.getByRole("button", { name: "View the pinned message in the timeline." }));
expect(screen.getByText("First pinned message")).toBeVisible();
@ -182,14 +185,14 @@ describe("<PinnedMessageBanner />", () => {
["m.audio", "Audio"],
["m.video", "Video"],
["m.image", "Image"],
])("should display the %s event type", (msgType, label) => {
])("should display the %s event type", async (msgType, label) => {
const body = `Message with ${msgType} type`;
const event = makePinEvent({ content: { body, msgtype: msgType } });
jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event.getId()!]);
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event]);
const { asFragment } = renderBanner();
expect(screen.getByTestId("banner-message")).toHaveTextContent(`${label}: ${body}`);
await expect(screen.findByTestId("banner-message")).resolves.toHaveTextContent(`${label}: ${body}`);
expect(asFragment()).toMatchSnapshot();
});
@ -199,7 +202,7 @@ describe("<PinnedMessageBanner />", () => {
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event]);
const { asFragment } = renderBanner();
expect(screen.getByTestId("banner-message")).toHaveTextContent("Poll: Alice?");
await expect(screen.findByTestId("banner-message")).resolves.toHaveTextContent("Poll: Alice?");
expect(asFragment()).toMatchSnapshot();
});

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

@ -37,12 +37,12 @@ exports[`<PinnedMessageBanner /> should display display a poll event 1`] = `
/>
</svg>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
class="mx_EventPreview_prefix"
>
Poll:
</span>
@ -113,8 +113,9 @@ exports[`<PinnedMessageBanner /> should display the last message when the pinned
</span>
</div>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
title="Third pinned message"
>
Third pinned message
</span>
@ -170,12 +171,12 @@ exports[`<PinnedMessageBanner /> should display the m.audio event type 1`] = `
/>
</svg>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
class="mx_EventPreview_prefix"
>
Audio:
</span>
@ -225,12 +226,12 @@ exports[`<PinnedMessageBanner /> should display the m.file event type 1`] = `
/>
</svg>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
class="mx_EventPreview_prefix"
>
File:
</span>
@ -280,12 +281,12 @@ exports[`<PinnedMessageBanner /> should display the m.image event type 1`] = `
/>
</svg>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
class="mx_EventPreview_prefix"
>
Image:
</span>
@ -335,12 +336,12 @@ exports[`<PinnedMessageBanner /> should display the m.video event type 1`] = `
/>
</svg>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
class="mx_EventPreview_prefix"
>
Video:
</span>
@ -407,8 +408,9 @@ exports[`<PinnedMessageBanner /> should render 2 pinned event 1`] = `
</span>
</div>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
title="Second pinned message"
>
Second pinned message
</span>
@ -485,8 +487,9 @@ exports[`<PinnedMessageBanner /> should render 4 pinned event 1`] = `
</span>
</div>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
title="Fourth pinned message"
>
Fourth pinned message
</span>
@ -542,8 +545,9 @@ exports[`<PinnedMessageBanner /> should render a single pinned event 1`] = `
/>
</svg>
<span
class="mx_PinnedMessageBanner_message"
class="mx_EventPreview mx_PinnedMessageBanner_message"
data-testid="banner-message"
title="First pinned message"
>
First pinned message
</span>

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();
@ -417,7 +391,7 @@ describe("message", () => {
expect(mockClient.sendMessage).toHaveBeenCalledTimes(0);
expect(mockClient.cancelPendingEvent).toHaveBeenCalledTimes(1);
expect(mockCreateRedactEventDialog).toHaveBeenCalledTimes(1);
expect(spyDispatcher).toHaveBeenCalledTimes(0);
expect(spyDispatcher).toHaveBeenCalledTimes(1);
});
it("Should do nothing if the content is unmodified", async () => {

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,13 +7,14 @@ 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";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import * as TestUtils from "../../../../test-utils";
import CryptographyPanel from "../../../../../src/components/views/settings/CryptographyPanel";
import { withClientContextRenderOptions } from "../../../../test-utils";
describe("CryptographyPanel", () => {
it("shows the session ID and key", async () => {
@ -28,7 +29,7 @@ describe("CryptographyPanel", () => {
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
// When we render the CryptographyPanel
const rendered = render(<CryptographyPanel />);
const rendered = render(<CryptographyPanel />, withClientContextRenderOptions(client));
// Then it displays info about the user's session
const codes = rendered.container.querySelectorAll("code");
@ -52,7 +53,7 @@ describe("CryptographyPanel", () => {
mocked(client.getCrypto()!.getOwnDeviceKeys).mockRejectedValue(new Error("bleh"));
// When we render the CryptographyPanel
const rendered = render(<CryptographyPanel />);
const rendered = render(<CryptographyPanel />, withClientContextRenderOptions(client));
// Then it displays info about the user's session
const codes = rendered.container.querySelectorAll("code");
@ -63,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,
@ -202,7 +202,7 @@ describe("<JoinRuleSettings />", () => {
await flushPromises();
expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
await expect(within(dialog).findByText("Loading new room")).resolves.toBeInTheDocument();
// "create" our new room, have it come thru sync
client.getRoom.mockImplementation((id) => {
@ -250,7 +250,7 @@ describe("<JoinRuleSettings />", () => {
await flushPromises();
expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
await expect(within(dialog).findByText("Loading new room")).resolves.toBeInTheDocument();
// "create" our new room, have it come thru sync
client.getRoom.mockImplementation((id) => {

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

@ -7,20 +7,18 @@ Please see LICENSE files in the repository root for full details.
*/
import { cleanup, render, waitFor } from "jest-matrix-react";
import { MockedObject, mocked } from "jest-mock";
import { mocked, MockedObject } from "jest-mock";
import React from "react";
import {
MSC3906Rendezvous,
LegacyRendezvousFailureReason,
ClientRendezvousFailureReason,
MSC4108SignInWithQR,
MSC4108FailureReason,
MSC4108SignInWithQR,
RendezvousError,
} from "matrix-js-sdk/src/rendezvous";
import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix";
import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
import LoginWithQR from "../../../../../../src/components/views/auth/LoginWithQR";
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
jest.mock("matrix-js-sdk/src/rendezvous");
jest.mock("matrix-js-sdk/src/rendezvous/transports");
@ -65,9 +63,6 @@ describe("<LoginWithQR />", () => {
mode: Mode.Show,
onFinished: jest.fn(),
};
const mockConfirmationDigits = "mock-confirmation-digits";
const mockRendezvousCode = "mock-rendezvous-code";
const newDeviceId = "new-device-id";
beforeEach(() => {
mockedFlow.mockReset();
@ -82,264 +77,10 @@ describe("<LoginWithQR />", () => {
cleanup();
});
describe("MSC3906", () => {
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
<React.StrictMode>
<LoginWithQR {...defaultProps} {...props} />
</React.StrictMode>
);
beforeEach(() => {
jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue();
// @ts-ignore
// workaround for https://github.com/facebook/jest/issues/9675
MSC3906Rendezvous.prototype.code = mockRendezvousCode;
jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue();
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits);
jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue();
jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId);
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined);
client.requestLoginToken.mockResolvedValue({
login_token: "token",
expires_in_ms: 1000 * 1000,
} as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet
});
test("no homeserver support", async () => {
// simulate no support
jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockRejectedValue("");
render(getComponent({ client }));
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.Error,
failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport,
onClick: expect.any(Function),
}),
);
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
expect(rendezvous.generateCode).toHaveBeenCalled();
});
test("failed to connect", async () => {
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue("");
render(getComponent({ client }));
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.Error,
failureReason: ClientRendezvousFailureReason.Unknown,
onClick: expect.any(Function),
}),
);
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
});
test("render QR then back", async () => {
const onFinished = jest.fn();
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise());
render(getComponent({ client, onFinished }));
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.ShowingQR,
}),
),
);
// display QR code
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.ShowingQR,
code: mockRendezvousCode,
onClick: expect.any(Function),
});
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
// back
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Back);
expect(onFinished).toHaveBeenCalledWith(false);
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
});
test("render QR then decline", async () => {
const onFinished = jest.fn();
render(getComponent({ client, onFinished }));
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.LegacyConnected,
}),
),
);
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.LegacyConnected,
confirmationDigits: mockConfirmationDigits,
onClick: expect.any(Function),
});
// decline
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Decline);
expect(onFinished).toHaveBeenCalledWith(false);
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
});
test("approve - no crypto", async () => {
(client as any).getCrypto = () => undefined;
const onFinished = jest.fn();
render(getComponent({ client, onFinished }));
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.LegacyConnected,
}),
),
);
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.LegacyConnected,
confirmationDigits: mockConfirmationDigits,
onClick: expect.any(Function),
});
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
// approve
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.WaitingForDevice,
}),
),
);
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
expect(onFinished).toHaveBeenCalledWith(true);
});
test("approve + verifying", async () => {
const onFinished = jest.fn();
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() =>
unresolvedPromise(),
);
render(getComponent({ client, onFinished }));
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.LegacyConnected,
}),
),
);
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.LegacyConnected,
confirmationDigits: mockConfirmationDigits,
onClick: expect.any(Function),
});
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
// approve
const onClick = mockedFlow.mock.calls[0][0].onClick;
onClick(Click.Approve);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.Verifying,
}),
),
);
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
// expect(onFinished).toHaveBeenCalledWith(true);
});
test("approve + verify", async () => {
const onFinished = jest.fn();
render(getComponent({ client, onFinished }));
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.LegacyConnected,
}),
),
);
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.LegacyConnected,
confirmationDigits: mockConfirmationDigits,
onClick: expect.any(Function),
});
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
// approve
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve);
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
expect(rendezvous.close).toHaveBeenCalled();
expect(onFinished).toHaveBeenCalledWith(true);
});
test("approve - rate limited", async () => {
mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429));
const onFinished = jest.fn();
render(getComponent({ client, onFinished }));
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.LegacyConnected,
}),
),
);
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.LegacyConnected,
confirmationDigits: mockConfirmationDigits,
onClick: expect.any(Function),
});
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
// approve
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve);
// the 429 error should be handled and mapped
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.Error,
failureReason: "rate_limited",
}),
),
);
});
});
describe("MSC4108", () => {
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
<React.StrictMode>
<LoginWithQR {...defaultProps} {...props} legacy={false} />
<LoginWithQR {...defaultProps} {...props} />
</React.StrictMode>
);
@ -363,7 +104,7 @@ describe("<LoginWithQR />", () => {
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Back);
expect(onFinished).toHaveBeenCalledWith(false);
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
});
test("failed to connect", async () => {
@ -404,6 +145,27 @@ describe("<LoginWithQR />", () => {
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
});
test("handles errors during protocol negotiation", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol);
// @ts-ignore work-around for lazy mocks
err.code = MSC4108FailureReason.UnsupportedProtocol;
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockRejectedValue(err);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.ShowingQR,
}),
),
);
await waitFor(() => {
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
});
});
test("handles errors during reciprocation", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});

View file

@ -8,11 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { cleanup, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import React from "react";
import {
ClientRendezvousFailureReason,
LegacyRendezvousFailureReason,
MSC4108FailureReason,
} from "matrix-js-sdk/src/rendezvous";
import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow";
import { LoginWithQRFailureReason, FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
@ -29,8 +25,7 @@ describe("<LoginWithQRFlow />", () => {
phase: Phase;
onClick?: () => Promise<void>;
failureReason?: FailureReason;
code?: string;
confirmationDigits?: string;
code?: Uint8Array;
}) => <LoginWithQRFlow {...defaultProps} {...props} />;
beforeEach(() => {});
@ -54,24 +49,14 @@ describe("<LoginWithQRFlow />", () => {
});
it("renders QR code", async () => {
const { container } = render(getComponent({ phase: Phase.ShowingQR, code: "mock-code" }));
const { container } = render(
getComponent({ phase: Phase.ShowingQR, code: new TextEncoder().encode("mock-code") }),
);
// QR code is rendered async so we wait for it:
await waitFor(() => screen.getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
it("renders code when connected", async () => {
const { container } = render(getComponent({ phase: Phase.LegacyConnected, confirmationDigits: "mock-digits" }));
expect(screen.getAllByText("mock-digits")).toHaveLength(1);
expect(screen.getAllByTestId("decline-login-button")).toHaveLength(1);
expect(screen.getAllByTestId("approve-login-button")).toHaveLength(1);
expect(container).toMatchSnapshot();
fireEvent.click(screen.getByTestId("decline-login-button"));
expect(onClick).toHaveBeenCalledWith(Click.Decline, undefined);
fireEvent.click(screen.getByTestId("approve-login-button"));
expect(onClick).toHaveBeenCalledWith(Click.Approve, undefined);
});
it("renders spinner while signing in", async () => {
const { container } = render(getComponent({ phase: Phase.WaitingForDevice }));
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
@ -92,7 +77,6 @@ describe("<LoginWithQRFlow />", () => {
describe("errors", () => {
for (const failureReason of [
...Object.values(LegacyRendezvousFailureReason),
...Object.values(MSC4108FailureReason),
...Object.values(LoginWithQRFailureReason),
...Object.values(ClientRendezvousFailureReason),

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { IClientWellKnown, IServerVersions, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "matrix-js-sdk/src/matrix";
import { IClientWellKnown, IServerVersions, MatrixClient } from "matrix-js-sdk/src/matrix";
import React from "react";
import fetchMock from "fetch-mock-jest";
@ -51,73 +51,6 @@ describe("<LoginWithQRSection />", () => {
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient({}));
});
describe("MSC3906", () => {
const defaultProps = {
onShowQr: () => {},
versions: makeVersions({}),
wellKnown: {},
};
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;
describe("should not render", () => {
it("no support at all", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("only get_login_token enabled", async () => {
const { container } = render(
getComponent({ capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } } }),
);
expect(container).toMatchSnapshot();
});
it("MSC3886 + get_login_token disabled", async () => {
const { container } = render(
getComponent({
versions: makeVersions({ "org.matrix.msc3886": true }),
capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: false } },
}),
);
expect(container).toMatchSnapshot();
});
});
describe("should render panel", () => {
it("get_login_token + MSC3886", async () => {
const { container } = render(
getComponent({
versions: makeVersions({
"org.matrix.msc3886": true,
}),
capabilities: {
[GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true },
},
}),
);
expect(container).toMatchSnapshot();
});
it("get_login_token + .well-known", async () => {
const wellKnown = {
"io.element.rendezvous": {
server: "https://rz.local",
},
};
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient(wellKnown));
const { container } = render(
getComponent({
versions: makeVersions({}),
capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } },
wellKnown,
}),
);
expect(container).toMatchSnapshot();
});
});
});
describe("MSC4108", () => {
describe("MSC4108", () => {
const defaultProps = {

View file

@ -49,7 +49,7 @@ HTMLCollection [
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
Verify or sign out from this session for best security and reliability.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
@ -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

@ -49,7 +49,7 @@ exports[`<DeviceDetails /> renders a verified device 1`] = `
<p
class="mx_DeviceSecurityCard_description"
>
Your current session is ready for secure messaging.
This session is ready for secure messaging.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
@ -158,7 +158,7 @@ exports[`<DeviceDetails /> renders device with metadata 1`] = `
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
Verify or sign out from this session for best security and reliability.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
@ -367,7 +367,7 @@ exports[`<DeviceDetails /> renders device without metadata 1`] = `
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
Verify or sign out from this session for best security and reliability.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"

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

@ -252,48 +252,6 @@ exports[`<LoginWithQRFlow /> errors renders expired 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders expired 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
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>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
The sign in was not completed in time
</h1>
<p
data-testid="cancellation-message"
>
Sign in expired. Please try again.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
<div>
<div
@ -374,86 +332,6 @@ exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_heading"
>
<div
aria-label="Back"
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
data-testid="back-button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.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.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</div>
<div
class="mx_LoginWithQR_breadcrumbs"
>
Sessions
/
Link new device
</div>
</div>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
QR code not supported
</h1>
<p
data-testid="cancellation-message"
>
Your account provider doesn't support signing into a new device with a QR code.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders insecure_channel_detected 1`] = `
<div>
<div
@ -761,90 +639,6 @@ exports[`<LoginWithQRFlow /> errors renders unknown 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unknown 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
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>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p
data-testid="cancellation-message"
>
An unexpected error occurred. The request to connect your other device has been cancelled.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unsupported_algorithm 1`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
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>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p
data-testid="cancellation-message"
>
An unexpected error occurred. The request to connect your other device has been cancelled.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unsupported_protocol 1`] = `
<div>
<div
@ -887,48 +681,6 @@ exports[`<LoginWithQRFlow /> errors renders unsupported_protocol 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders unsupported_protocol 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
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>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Other device not compatible
</h1>
<p
data-testid="cancellation-message"
>
This device does not support signing in to the other device with a QR code.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
<div>
<div
@ -971,48 +723,6 @@ exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_cancelled 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
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>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Sign in request cancelled
</h1>
<p
data-testid="cancellation-message"
>
The sign in was cancelled on the other device.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = `
<div>
<div
@ -1055,48 +765,6 @@ exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> errors renders user_declined 2`] = `
<div>
<div
class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<svg
fill="currentColor"
height="32px"
viewBox="0 0 24 24"
width="32px"
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>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Sign in declined
</h1>
<p
data-testid="cancellation-message"
>
You or the account provider declined the sign in request.
</p>
</div>
<div
class="mx_LoginWithQR_buttons"
/>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> renders QR code 1`] = `
<div>
<div
@ -1256,58 +924,6 @@ exports[`<LoginWithQRFlow /> renders check code confirmation 1`] = `
</div>
`;
exports[`<LoginWithQRFlow /> renders code when connected 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_main"
>
<p>
Check that the code below matches with your other device:
</p>
<div
class="mx_LoginWithQR_confirmationDigits"
>
mock-digits
</div>
<div
class="mx_LoginWithQR_confirmationAlert"
>
<div>
<div />
</div>
<div>
By approving access for this device, it will have full access to your account.
</div>
</div>
</div>
<div
class="mx_LoginWithQR_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="approve-login-button"
role="button"
tabindex="0"
>
Approve
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
data-testid="decline-login-button"
role="button"
tabindex="0"
>
Cancel
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRFlow /> renders spinner while loading 1`] = `
<div>
<div

View file

@ -1,307 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LoginWithQRSection /> MSC3906 should not render MSC3886 + get_login_token disabled 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
<p
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
>
Not supported by your account provider
</p>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should not render no support at all 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
<p
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
>
Not supported by your account provider
</p>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should not render only get_login_token enabled 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
<p
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
>
Not supported by your account provider
</p>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should render panel get_login_token + .well-known 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
</div>
</div>
</div>
</div>
`;
exports[`<LoginWithQRSection /> MSC3906 should render panel get_login_token + MSC3886 1`] = `
<div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Link new device
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_LoginWithQRSection"
>
<p
class="mx_SettingsTab_subsectionText"
>
Use a QR code to sign in to another device and set up secure messaging.
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
fill-rule="evenodd"
/>
<path
d="M15 16v-3h-2v3h2Z"
/>
<path
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
/>
</svg>
Show QR code
</div>
</div>
</div>
</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

@ -10,28 +10,17 @@ import React from "react";
import { fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
import { EventType, GuestAccess, HistoryVisibility, JoinRule, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { mocked } from "jest-mock";
import SecurityRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/SecurityRoomSettingsTab";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import {
clearAllModals,
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsUser,
} from "../../../../../../test-utils";
import { clearAllModals, flushPromises, stubClient } from "../../../../../../test-utils";
import { filterBoolean } from "../../../../../../../src/utils/arrays";
describe("<SecurityRoomSettingsTab />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
isRoomEncrypted: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
sendStateEvent: jest.fn(),
getClientWellKnown: jest.fn(),
});
const client = mocked(stubClient());
const roomId = "!room:server.org";
const getComponent = (room: Room, closeSettingsFn = jest.fn()) =>
@ -96,11 +85,12 @@ describe("<SecurityRoomSettingsTab />", () => {
describe("join rule", () => {
it("warns when trying to make an encrypted room public", async () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
fireEvent.click(screen.getByLabelText("Public"));
const modal = await screen.findByRole("dialog");
@ -244,19 +234,21 @@ describe("<SecurityRoomSettingsTab />", () => {
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
});
it("does not render world readable option when room is encrypted", () => {
it("does not render world readable option when room is encrypted", async () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument();
await waitFor(() =>
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument(),
);
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
@ -305,13 +297,13 @@ describe("<SecurityRoomSettingsTab />", () => {
});
describe("encryption", () => {
it("displays encryption as enabled", () => {
it("displays encryption as enabled", async () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
// can't disable encryption once enabled
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
});
@ -356,7 +348,7 @@ describe("<SecurityRoomSettingsTab />", () => {
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
@ -412,21 +404,20 @@ describe("<SecurityRoomSettingsTab />", () => {
});
});
it("displays encrypted rooms as encrypted", () => {
it("displays encrypted rooms as encrypted", async () => {
// rooms that are already encrypted still show encrypted
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
});
it("displays unencrypted rooms with toggle disabled", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(false);
setRoomStateEvents(room);
getComponent(room);

View file

@ -119,6 +119,7 @@ describe("<SessionManagerTab />", () => {
const mockVerificationRequest = {
cancel: jest.fn(),
on: jest.fn(),
off: jest.fn(),
} as unknown as VerificationRequest;
const mockCrypto = mocked({
@ -1650,46 +1651,6 @@ describe("<SessionManagerTab />", () => {
expect(checkbox.getAttribute("aria-checked")).toEqual("false");
});
describe("MSC3906 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(false);
// enable server support for qr login
mockClient.getVersions.mockResolvedValue({
versions: [],
unstable_features: {
"org.matrix.msc3886": true,
},
});
mockClient.getCapabilities.mockResolvedValue({
[GET_LOGIN_TOKEN_CAPABILITY.name]: {
enabled: true,
},
});
});
it("renders qr code login section", async () => {
const { getByText } = render(getComponent());
// wait for versions call to settle
await flushPromises();
expect(getByText("Link new device")).toBeTruthy();
expect(getByText("Show QR code")).toBeTruthy();
});
it("enters qr code login section when show QR code button clicked", async () => {
const { getByText, findByTestId } = render(getComponent());
// wait for versions call to settle
await flushPromises();
fireEvent.click(getByText("Show QR code"));
await expect(findByTestId("login-with-qr")).resolves.toBeTruthy();
});
});
describe("MSC4108 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
const issuer = "https://issuer.org";

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>

View file

@ -61,7 +61,17 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings with guest spa url
<div
class="mx_SettingsSubsection_text"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.971 3.54 7 3.889A2 2 0 0 1 21 9.177V19a2 2 0 0 1-2 2h-4v-9H9v9H5a2 2 0 0 1-2-2V9.177a2 2 0 0 1 1.029-1.748l7-3.89a2 2 0 0 1 1.942 0Z"
/>
</svg>
Home
</div>
<div
@ -312,7 +322,17 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings without guest spa u
<div
class="mx_SettingsSubsection_text"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.971 3.54 7 3.889A2 2 0 0 1 21 9.177V19a2 2 0 0 1-2 2h-4v-9H9v9H5a2 2 0 0 1-2-2V9.177a2 2 0 0 1 1.029-1.748l7-3.89a2 2 0 0 1 1.942 0Z"
/>
</svg>
Home
</div>
<div

View file

@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { mocked } from "jest-mock";
import EditorModel from "../../../src/editor/model";
import { htmlSerializeIfNeeded } from "../../../src/editor/serialize";
import { htmlSerializeFromMdIfNeeded, htmlSerializeIfNeeded } from "../../../src/editor/serialize";
import { createPartCreator } from "./mock";
import { IConfigOptions } from "../../../src/IConfigOptions";
import SettingsStore from "../../../src/settings/SettingsStore";
@ -71,6 +71,12 @@ describe("editor/serialize", function () {
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe("*hello* world");
});
it("escaped markdown should not retain backslashes around other markdown", function () {
const pc = createPartCreator();
const model = new EditorModel([pc.plain("\\*hello\\* **world**")], pc);
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe("*hello* <strong>world</strong>");
});
it("escaped markdown should convert HTML entities", function () {
const pc = createPartCreator();
const model = new EditorModel([pc.plain("\\*hello\\* world < hey world!")], pc);
@ -153,6 +159,14 @@ describe("editor/serialize", function () {
const html = htmlSerializeIfNeeded(model, { forceHTML: true, useMarkdown: false });
expect(html).toBe("hello world");
});
it("should treat tags not in allowlist as plaintext", () => {
const html = htmlSerializeFromMdIfNeeded("<b>test</b>", {});
expect(html).toBeUndefined();
});
it("should treat tags not in allowlist as plaintext even if escaped", () => {
const html = htmlSerializeFromMdIfNeeded("\\<b>test</b>", {});
expect(html).toBe("&lt;b&gt;test&lt;/b&gt;");
});
});
describe("feature_latex_maths", () => {

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

@ -170,6 +170,106 @@ describe("StopGapWidgetDriver", () => {
expect(listener).toHaveBeenCalledWith(openIdUpdate);
});
describe("sendToDevice", () => {
const contentMap = {
"@alice:example.org": {
"*": {
hello: "alice",
},
},
"@bob:example.org": {
bobDesktop: {
hello: "bob",
},
},
};
let driver: WidgetDriver;
beforeEach(() => {
driver = mkDefaultDriver();
});
it("sends unencrypted messages", async () => {
await driver.sendToDevice("org.example.foo", false, contentMap);
expect(client.queueToDevice).toHaveBeenCalledWith({
eventType: "org.example.foo",
batch: [
{ deviceId: "*", payload: { hello: "alice" }, userId: "@alice:example.org" },
{ deviceId: "bobDesktop", payload: { hello: "bob" }, userId: "@bob:example.org" },
],
});
});
it("sends encrypted messages", async () => {
const encryptToDeviceMessages = jest
.fn()
.mockImplementation(
(eventType, recipients: { userId: string; deviceId: string }[], content: object) => ({
eventType: "m.room.encrypted",
batch: recipients.map(({ userId, deviceId }) => ({
userId,
deviceId,
payload: {
eventType,
content,
},
})),
}),
);
MatrixClientPeg.safeGet().getCrypto()!.encryptToDeviceMessages = encryptToDeviceMessages;
await driver.sendToDevice("org.example.foo", true, {
"@alice:example.org": {
aliceMobile: {
hello: "alice",
},
},
"@bob:example.org": {
bobDesktop: {
hello: "bob",
},
},
});
expect(encryptToDeviceMessages).toHaveBeenCalledWith(
"org.example.foo",
[{ deviceId: "aliceMobile", userId: "@alice:example.org" }],
{
hello: "alice",
},
);
expect(encryptToDeviceMessages).toHaveBeenCalledWith(
"org.example.foo",
[{ deviceId: "bobDesktop", userId: "@bob:example.org" }],
{
hello: "bob",
},
);
expect(client.queueToDevice).toHaveBeenCalledWith({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
deviceId: "aliceMobile",
payload: { content: { hello: "alice" }, eventType: "org.example.foo" },
userId: "@alice:example.org",
},
]),
});
expect(client.queueToDevice).toHaveBeenCalledWith({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
deviceId: "bobDesktop",
payload: { content: { hello: "bob" }, eventType: "org.example.foo" },
userId: "@bob:example.org",
},
]),
});
});
});
describe("getTurnServers", () => {
let driver: WidgetDriver;

View file

@ -17,24 +17,19 @@ exports[`HTMLExport should export 1`] = `
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
<div class="mx_MatrixChat">
<main class="mx_RoomView">
<div class="mx_LegacyRoomHeader light-panel">
<div class="mx_LegacyRoomHeader_wrapper" aria-owns="mx_RightPanel">
<div class="mx_LegacyRoomHeader_avatar">
<div class="mx_DecoratedRoomAvatar">
<span role="presentation" title="!myroom:example.org" data-testid="avatar-img" data-type="round" data-color="1" class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" style="--cpd-avatar-size:32px">!</span>
<div class="mx_Flex mx_RoomHeader light-panel">
<span role="presentation" title="!myroom:example.org" data-testid="avatar-img" data-type="round" data-color="1" class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" style="--cpd-avatar-size:32px">!</span>
<div class="mx_RoomHeader_infoWrapper">
<div
dir="auto"
class="mx_RoomHeader_info"
title="!myroom:example.org"
>
<span class="mx_RoomHeader_truncated mx_lineClamp">
!myroom:example.org
</span>
</div>
</div>
</div>
<div class="mx_LegacyRoomHeader_name">
<div
dir="auto"
class="mx_LegacyRoomHeader_nametext"
title="!myroom:example.org"
>
!myroom:example.org
</div>
</div>
<div class="mx_LegacyRoomHeader_topic" dir="auto"> </div>
</div>
</div>
<div class="mx_MainSplit">

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

@ -19,9 +19,9 @@ describe("requestMediaPermissions", () => {
const audioStream = {} as MediaStream;
const itShouldLogTheErrorAndShowTheNoMediaPermissionsModal = () => {
it("should log the error and show the »No media permissions« modal", () => {
it("should log the error and show the »No media permissions« modal", async () => {
expect(logger.log).toHaveBeenCalledWith("Failed to list userMedia devices", error);
screen.getByText("No media permissions");
await screen.findByText("No media permissions");
});
};

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

@ -229,4 +229,18 @@ describe("WebPlatform", () => {
});
});
});
it("should return config from config.json", async () => {
window.location.hostname = "domain.com";
fetchMock.get(/config\.json.*/, { brand: "test" });
const platform = new WebPlatform();
await expect(platform.getConfig()).resolves.toEqual(expect.objectContaining({ brand: "test" }));
});
it("should re-render favicon when setting error status", () => {
const platform = new WebPlatform();
const spy = jest.spyOn(platform.favicon, "badge");
platform.setErrorStatus(true);
expect(spy).toHaveBeenCalledWith(expect.anything(), { bgColor: "#f00" });
});
});

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" });
});
});