Merge branch 'develop' of https://github.com/vector-im/element-web into dbkr/stateafter

# Conflicts:
#	test/unit-tests/components/structures/RoomView-test.tsx
#	test/unit-tests/components/structures/TimelinePanel-test.tsx
This commit is contained in:
Michael Telatynski 2024-11-27 10:47:35 +00:00
commit 3dbcb5efa3
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
438 changed files with 7829 additions and 4692 deletions

View file

@ -143,7 +143,6 @@ export const mockClientMethodsCrypto = (): Partial<
> => ({
isKeyBackupKeyStored: jest.fn(),
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
secretStorage: { hasKey: jest.fn() },
getCrypto: jest.fn().mockReturnValue({
getUserDeviceInfo: jest.fn(),
@ -162,6 +161,8 @@ export const mockClientMethodsCrypto = (): Partial<
getVersion: jest.fn().mockReturnValue("Version 0"),
getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})),
getCrossSigningKeyId: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
}),
});

View file

@ -27,7 +27,6 @@ const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
const customRender = (ui: ReactElement, options: RenderOptions = {}) => {
return render(ui, {
legacyRoot: true,
...options,
wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"],
}) as ReturnType<typeof render>;

View file

@ -18,7 +18,7 @@ import {
M_POLL_RESPONSE,
M_TEXT,
} from "matrix-js-sdk/src/matrix";
import { uuid4 } from "@sentry/utils";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { flushPromises } from "./utilities";
@ -67,7 +67,7 @@ export const makePollEndEvent = (
id?: string,
): MatrixEvent => {
return new MatrixEvent({
event_id: id || uuid4(),
event_id: id || randomString(16),
room_id: roomId,
origin_server_ts: ts,
type: M_POLL_END.name,
@ -91,7 +91,7 @@ export const makePollResponseEvent = (
ts = 0,
): MatrixEvent =>
new MatrixEvent({
event_id: uuid4(),
event_id: randomString(16),
room_id: roomId,
origin_server_ts: ts,
type: M_POLL_RESPONSE.name,

View file

@ -85,7 +85,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
...override,
};
}

View file

@ -99,7 +99,6 @@ export function createTestClient(): MatrixClient {
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
credentials: { userId: "@userId:matrix.org" },
getKeyBackupVersion: jest.fn(),
secretStorage: {
get: jest.fn(),
@ -117,7 +116,7 @@ export function createTestClient(): MatrixClient {
getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn(),
getUserDeviceInfo: jest.fn(),
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(),
@ -131,6 +130,11 @@ export function createTestClient(): MatrixClient {
createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({}),
bootstrapSecretStorage: jest.fn(),
isDehydrationSupported: jest.fn().mockResolvedValue(false),
restoreKeyBackup: jest.fn(),
restoreKeyBackupWithPassphrase: jest.fn(),
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
storeSessionBackupPrivateKey: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
}),
getPushActionsForEvent: jest.fn(),
@ -275,6 +279,7 @@ export function createTestClient(): MatrixClient {
sendStickerMessage: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
uploadDeviceSigningKeys: jest.fn(),
isKeyBackupKeyStored: jest.fn().mockResolvedValue(null),
} as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client);

View file

@ -197,7 +197,7 @@ export const clearAllModals = async (): Promise<void> => {
// Prevent modals from leaking and polluting other tests
let keepClosingModals = true;
while (keepClosingModals) {
keepClosingModals = Modal.closeCurrentModal();
keepClosingModals = await act(() => Modal.closeCurrentModal());
// Then wait for the screen to update (probably React rerender and async/await).
// Important for tests using Jest fake timers to not get into an infinite loop

View file

@ -95,17 +95,17 @@ describe("DeviceListener", () => {
},
}),
getSessionBackupPrivateKey: jest.fn(),
isEncryptionEnabledInRoom: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<CryptoApi>;
mockClient = getMockClientWithEventEmitter({
isGuest: jest.fn(),
getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId),
getKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
getRooms: jest.fn().mockReturnValue([]),
isVersionSupported: jest.fn().mockResolvedValue(true),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
waitForClientWellKnown: jest.fn(),
isRoomEncrypted: jest.fn(),
getClientWellKnown: jest.fn(),
getDeviceId: jest.fn().mockReturnValue(deviceId),
setAccountData: jest.fn(),
@ -292,7 +292,7 @@ describe("DeviceListener", () => {
mockCrypto!.isCrossSigningReady.mockResolvedValue(false);
mockCrypto!.isSecretStorageReady.mockResolvedValue(false);
mockClient!.getRooms.mockReturnValue(rooms);
mockClient!.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
});
it("hides setup encryption toast when cross signing and secret storage are ready", async () => {
@ -317,7 +317,7 @@ describe("DeviceListener", () => {
});
it("does not show any toasts when no rooms are encrypted", async () => {
mockClient!.isRoomEncrypted.mockReturnValue(false);
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
await createAndStart();
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
@ -354,7 +354,7 @@ describe("DeviceListener", () => {
it("shows set up encryption toast when user has a key backup available", async () => {
// non falsy response
mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo);
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
await createAndStart();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
@ -673,7 +673,7 @@ describe("DeviceListener", () => {
describe("When Room Key Backup is not enabled", () => {
beforeEach(() => {
// no backup
mockClient.getKeyBackupVersion.mockResolvedValue(null);
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
});
it("Should report recovery state as Enabled", async () => {
@ -722,7 +722,7 @@ describe("DeviceListener", () => {
});
// no backup
mockClient.getKeyBackupVersion.mockResolvedValue(null);
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
await createAndStart();
@ -872,7 +872,7 @@ describe("DeviceListener", () => {
describe("When Room Key Backup is enabled", () => {
beforeEach(() => {
// backup enabled - just need a mock object
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
});
const testCases = [

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, { forceReset: 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",
// 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",
// 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 131 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70",
// Edge 131 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70",
// 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

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { HTMLAttributes } from "react";
import { render } from "jest-matrix-react";
import { act, render } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import {
@ -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>;
@ -73,15 +79,15 @@ describe("RovingTabIndex", () => {
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// focus on 2nd button and test it is the only active one
container.querySelectorAll("button")[2].focus();
act(() => container.querySelectorAll("button")[2].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
// focus on 1st button and test it is the only active one
container.querySelectorAll("button")[1].focus();
act(() => container.querySelectorAll("button")[1].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
// check that the active button does not change even on an explicit blur event
container.querySelectorAll("button")[1].blur();
act(() => container.querySelectorAll("button")[1].blur());
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
// update the children, it should remain on the same 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>
)}
@ -141,81 +162,81 @@ describe("RovingTabIndex", () => {
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// focus on 2nd button and test it is the only active one
container.querySelectorAll("button")[2].focus();
act(() => container.querySelectorAll("button")[2].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
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],
});
});
});
@ -369,7 +390,7 @@ describe("RovingTabIndex", () => {
</RovingTabIndexProvider>,
);
container.querySelectorAll("button")[0].focus();
act(() => container.querySelectorAll("button")[0].focus());
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
await userEvent.keyboard("[ArrowDown]");
@ -402,7 +423,7 @@ describe("RovingTabIndex", () => {
</RovingTabIndexProvider>,
);
container.querySelectorAll("button")[0].focus();
act(() => container.querySelectorAll("button")[0].focus());
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
const button = container.querySelectorAll("button")[1];

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

@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
import "core-js/stable/structured-clone";
import "fake-indexeddb/auto";
import React, { ComponentProps } from "react";
import { fireEvent, render, RenderResult, screen, waitFor, within } from "jest-matrix-react";
import { fireEvent, render, RenderResult, screen, waitFor, within, act } from "jest-matrix-react";
import fetchMock from "fetch-mock-jest";
import { Mocked, mocked } from "jest-mock";
import { ClientEvent, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
@ -139,6 +139,7 @@ describe("<MatrixChat />", () => {
globalBlacklistUnverifiedDevices: false,
// This needs to not finish immediately because we need to test the screen appears
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
}),
secretStorage: {
isStored: jest.fn().mockReturnValue(null),
@ -146,7 +147,6 @@ describe("<MatrixChat />", () => {
matrixRTC: createStubMatrixRTC(),
getDehydratedDevice: jest.fn(),
whoami: jest.fn(),
isRoomEncrypted: jest.fn(),
logout: jest.fn(),
getDeviceId: jest.fn(),
});
@ -162,8 +162,11 @@ describe("<MatrixChat />", () => {
};
let initPromise: Promise<void> | undefined;
let defaultProps: ComponentProps<typeof MatrixChat>;
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) =>
render(<MatrixChat {...defaultProps} {...props} />);
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) => {
// MatrixChat does many questionable things which bomb tests in modern React mode,
// we'll want to refactor and break up MatrixChat before turning off legacyRoot mode
return render(<MatrixChat {...defaultProps} {...props} />, { legacyRoot: true });
};
// make test results readable
filterConsole(
@ -201,7 +204,7 @@ describe("<MatrixChat />", () => {
// we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Syncing…");
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
await act(() => client.emit(ClientEvent.Sync, SyncState.Prepared, null));
}
// let things settle
@ -263,7 +266,7 @@ describe("<MatrixChat />", () => {
// emit a loggedOut event so that all of the Store singletons forget about their references to the mock client
// (must be sync otherwise the next test will start before it happens)
defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true);
act(() => defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true));
localStorage.clear();
});
@ -328,7 +331,7 @@ describe("<MatrixChat />", () => {
expect(within(dialog).getByText(errorMessage)).toBeInTheDocument();
// just check we're back on welcome page
await expect(await screen.findByTestId("mx_welcome_screen")).toBeInTheDocument();
await expect(screen.findByTestId("mx_welcome_screen")).resolves.toBeInTheDocument();
};
beforeEach(() => {
@ -956,9 +959,11 @@ describe("<MatrixChat />", () => {
await screen.findByText("Powered by Matrix");
// go to login page
defaultDispatcher.dispatch({
action: "start_login",
});
act(() =>
defaultDispatcher.dispatch({
action: "start_login",
}),
);
await flushPromises();
@ -1010,6 +1015,7 @@ describe("<MatrixChat />", () => {
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
// This needs to not finish immediately because we need to test the screen appears
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
};
loginClient.getCrypto.mockReturnValue(mockCrypto as any);
});
@ -1057,9 +1063,11 @@ describe("<MatrixChat />", () => {
},
});
loginClient.isRoomEncrypted.mockImplementation((roomId) => {
return roomId === encryptedRoom.roomId;
});
jest.spyOn(loginClient.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation(
async (roomId) => {
return roomId === encryptedRoom.roomId;
},
);
});
it("should go straight to logged in view when user is not in any encrypted rooms", async () => {
@ -1125,7 +1133,9 @@ describe("<MatrixChat />", () => {
bootstrapDeferred.resolve();
await expect(await screen.findByRole("heading", { name: "You're in", level: 1 })).toBeInTheDocument();
await expect(
screen.findByRole("heading", { name: "You're in", level: 1 }),
).resolves.toBeInTheDocument();
});
});
});
@ -1394,7 +1404,9 @@ describe("<MatrixChat />", () => {
function simulateSessionLockClaim() {
localStorage.setItem("react_sdk_session_lock_claimant", "testtest");
window.dispatchEvent(new StorageEvent("storage", { key: "react_sdk_session_lock_claimant" }));
act(() =>
window.dispatchEvent(new StorageEvent("storage", { key: "react_sdk_session_lock_claimant" })),
);
}
it("after a session is restored", async () => {
@ -1515,7 +1527,7 @@ describe("<MatrixChat />", () => {
describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => {
const spy = jest.spyOn(Modal, "createDialogAsync");
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__test: true,
__esModule: true,
@ -1530,7 +1542,25 @@ describe("<MatrixChat />", () => {
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect(await spy.mock.lastCall![0]).toEqual(expect.objectContaining({ __test: true }));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
it("should show the recovery method removed dialog", async () => {
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
getComponent({});
defaultDispatcher.dispatch({
action: "will_start_client",
});
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
});
});

View file

@ -23,6 +23,7 @@ import {
createTestClient,
getMockClientWithEventEmitter,
makeBeaconInfoEvent,
mockClientMethodsCrypto,
mockClientMethodsEvents,
mockClientMethodsUser,
} from "../../../test-utils";
@ -42,6 +43,7 @@ describe("MessagePanel", function () {
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsEvents(),
...mockClientMethodsCrypto(),
getAccountData: jest.fn(),
isUserIgnored: jest.fn().mockReturnValue(false),
isRoomEncrypted: jest.fn().mockReturnValue(false),

View file

@ -81,9 +81,7 @@ describe("PipContainer", () => {
let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore;
const actFlushPromises = async () => {
await act(async () => {
await flushPromises();
});
await flushPromises();
};
beforeEach(async () => {
@ -165,12 +163,12 @@ describe("PipContainer", () => {
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
const widget = new Widget(call.widget);
WidgetStore.instance.addVirtualWidget(call.widget, room.roomId);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
await act(async () => {
WidgetStore.instance.addVirtualWidget(call.widget, room.roomId);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
await call.start();
ActiveWidgetStore.instance.setWidgetPersistence(widget.id, room.roomId, true);
});
@ -178,9 +176,11 @@ describe("PipContainer", () => {
await fn(call);
cleanup();
call.destroy();
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
WidgetStore.instance.removeVirtualWidget(widget.id, room.roomId);
act(() => {
call.destroy();
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
WidgetStore.instance.removeVirtualWidget(widget.id, room.roomId);
});
};
const withWidget = async (fn: () => Promise<void>): Promise<void> => {

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

@ -10,26 +10,37 @@ import React, { createRef, RefObject } from "react";
import { mocked, MockedObject } from "jest-mock";
import {
ClientEvent,
EventTimeline,
EventType,
IEvent,
JoinRule,
MatrixClient,
MatrixError,
MatrixEvent,
Room,
RoomEvent,
EventType,
JoinRule,
MatrixError,
RoomStateEvent,
MatrixEvent,
SearchResult,
IEvent,
} from "matrix-js-sdk/src/matrix";
import { CryptoApi, UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import {
fireEvent,
render,
screen,
RenderResult,
waitForElementToBeRemoved,
waitFor,
act,
cleanup,
} from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { defer } from "matrix-js-sdk/src/utils";
import {
stubClient,
mockPlatformPeg,
unmockPlatformPeg,
wrapInMatrixClientContext,
flushPromises,
mkEvent,
setupAsyncStoreWithClient,
@ -42,9 +53,9 @@ 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 { RoomView } from "../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
@ -63,8 +74,7 @@ import WidgetStore from "../../../../src/stores/WidgetStore";
import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRoomErrorPayload";
import { SearchScope } from "../../../../src/Searching";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
const RoomView = wrapInMatrixClientContext(_RoomView);
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
describe("RoomView", () => {
let cli: MockedObject<MatrixClient>;
@ -72,14 +82,14 @@ describe("RoomView", () => {
let rooms: Map<string, Room>;
let roomCount = 0;
let stores: SdkContextClass;
let crypto: CryptoApi;
// mute some noise
filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability");
beforeEach(() => {
mockPlatformPeg({ reload: () => {} });
stubClient();
cli = mocked(MatrixClientPeg.safeGet());
cli = mocked(stubClient());
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
jest.spyOn(room, "findPredecessor");
@ -97,15 +107,17 @@ describe("RoomView", () => {
stores.rightPanelStore.useUnitTestClient(cli);
jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(undefined);
crypto = cli.getCrypto()!;
jest.spyOn(cli, "getCrypto").mockReturnValue(undefined);
});
afterEach(() => {
unmockPlatformPeg();
jest.clearAllMocks();
cleanup();
});
const mountRoomView = async (ref?: RefObject<_RoomView>): Promise<RenderResult> => {
const mountRoomView = async (ref?: RefObject<RoomView>): Promise<RenderResult> => {
if (stores.roomViewStore.getRoomId() !== room.roomId) {
const switchedRoom = new Promise<void>((resolve) => {
const subFn = () => {
@ -117,26 +129,30 @@ describe("RoomView", () => {
stores.roomViewStore.on(UPDATE_EVENT, subFn);
});
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: undefined,
});
act(() =>
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: undefined,
}),
);
await switchedRoom;
}
const roomView = render(
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
wrappedRef={ref as any}
/>
</SDKContext.Provider>,
<MatrixClientContext.Provider value={cli}>
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
ref={ref}
/>
</SDKContext.Provider>
</MatrixClientContext.Provider>,
);
await flushPromises();
return roomView;
@ -164,22 +180,24 @@ describe("RoomView", () => {
}
const roomView = render(
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
onRegistered={jest.fn()}
/>
</SDKContext.Provider>,
<MatrixClientContext.Provider value={cli}>
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
onRegistered={jest.fn()}
/>
</SDKContext.Provider>
</MatrixClientContext.Provider>,
);
await flushPromises();
return roomView;
};
const getRoomViewInstance = async (): Promise<_RoomView> => {
const ref = createRef<_RoomView>();
const getRoomViewInstance = async (): Promise<RoomView> => {
const ref = createRef<RoomView>();
await mountRoomView(ref);
return ref.current!;
};
@ -190,7 +208,7 @@ describe("RoomView", () => {
});
describe("when there is an old room", () => {
let instance: _RoomView;
let instance: RoomView;
let oldRoom: Room;
beforeEach(async () => {
@ -214,11 +232,11 @@ describe("RoomView", () => {
describe("and feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
instance.setState({ msc3946ProcessDynamicPredecessor: true });
act(() => instance.setState({ msc3946ProcessDynamicPredecessor: true }));
});
afterEach(() => {
instance.setState({ msc3946ProcessDynamicPredecessor: false });
act(() => instance.setState({ msc3946ProcessDynamicPredecessor: false }));
});
it("should pass the setting to findPredecessor", async () => {
@ -230,8 +248,9 @@ describe("RoomView", () => {
it("updates url preview visibility on encryption state change", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
// we should be starting unencrypted
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
expect(await cli.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)).toEqual(false);
const roomViewInstance = await getRoomViewInstance();
@ -246,35 +265,74 @@ describe("RoomView", () => {
expect(roomViewInstance.state.showUrlPreview).toBe(true);
// now enable encryption
cli.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
// and fake an encryption event into the room to prompt it to re-check
room.addLiveEvents(
[
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
state_key: "",
}),
],
{ addToState: true },
);
act(() => {
const encryptionEvent = new MatrixEvent({
type: EventType.RoomEncryption,
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
});
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
cli.emit(RoomStateEvent.Events, encryptionEvent, roomState, null);
});
// URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false);
await waitFor(() => expect(roomViewInstance.state.showUrlPreview).toBe(false));
});
it("should not display the timeline when the room encryption is loading", async () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
const deferred = defer<boolean>();
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation(() => deferred.promise);
const { asFragment, container } = await mountRoomView();
expect(container.querySelector(".mx_RoomView_messagePanel")).toBeNull();
expect(asFragment()).toMatchSnapshot();
deferred.resolve(true);
await waitFor(() => expect(container.querySelector(".mx_RoomView_messagePanel")).not.toBeNull());
expect(asFragment()).toMatchSnapshot();
});
it("updates live timeline when a timeline reset happens", async () => {
const roomViewInstance = await getRoomViewInstance();
const oldTimeline = roomViewInstance.state.liveTimeline;
room.getUnfilteredTimelineSet().resetLiveTimeline();
act(() => room.getUnfilteredTimelineSet().resetLiveTimeline());
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
});
it("should update when the e2e status when the user verification changed", async () => {
room.currentState.setStateEvents([
mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId),
mkRoomMemberJoinEvent("user@example.com", room.roomId),
]);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
// Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both.
mocked(cli.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false),
);
jest.spyOn(cli.getCrypto()!, "getUserDeviceInfo").mockResolvedValue(
new Map([["user@example.com", new Map<string, any>()]]),
);
const { container } = await renderRoomView();
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).toBeInTheDocument());
const verificationStatus = new UserVerificationStatus(true, true, false);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus);
cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus);
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument());
});
describe("with virtual rooms", () => {
it("checks for a virtual room on initial load", async () => {
const { container } = await renderRoomView();
@ -288,7 +346,7 @@ describe("RoomView", () => {
await renderRoomView();
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
cli.emit(ClientEvent.Room, room);
act(() => cli.emit(ClientEvent.Room, room));
// called again after room event
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2);
@ -345,7 +403,13 @@ describe("RoomView", () => {
describe("that is encrypted", () => {
beforeEach(() => {
// Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both.
mocked(cli.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, true, false),
);
localRoom.encrypted = true;
localRoom.currentState.setStateEvents([
new MatrixEvent({
@ -364,7 +428,7 @@ describe("RoomView", () => {
it("should match the snapshot", async () => {
const { container } = await renderRoomView();
expect(container).toMatchSnapshot();
await waitFor(() => expect(container).toMatchSnapshot());
});
});
});
@ -406,7 +470,8 @@ describe("RoomView", () => {
]);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId());
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
mocked(cli).isRoomEncrypted.mockReturnValue(true);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
await renderRoomView();
});
@ -424,6 +489,194 @@ describe("RoomView", () => {
});
});
it("should show error view if failed to look up room alias", async () => {
const { asFragment, findByText } = await renderRoomView(false);
act(() =>
defaultDispatcher.dispatch<ViewRoomErrorPayload>({
action: Action.ViewRoomError,
room_alias: "#addy:server",
room_id: null,
err: new MatrixError({ errcode: "M_NOT_FOUND" }),
}),
);
await emitPromise(stores.roomViewStore, UPDATE_EVENT);
await findByText("Are you sure you're at the right place?");
expect(asFragment()).toMatchSnapshot();
});
describe("knock rooms", () => {
const client = createTestClient();
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
jest.spyOn(defaultDispatcher, "dispatch");
});
it("allows to request to join", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "knockRoom").mockResolvedValue({ room_id: room.roomId });
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
await untilDispatch(Action.SubmitAskToJoin, defaultDispatcher);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "submit_ask_to_join",
roomId: room.roomId,
opts: { reason: undefined },
});
});
it("allows to cancel a join request", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "leave").mockResolvedValue({});
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Knock);
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
await untilDispatch(Action.CancelAskToJoin, defaultDispatcher);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "cancel_ask_to_join",
roomId: room.roomId,
});
});
});
it("should close search results when edit is clicked", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.Room,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await prom;
});
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
rooms.set(room2.roomId, room2);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.All,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room2.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = untilDispatch(Action.ViewRoom, defaultDispatcher);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
});
it("fires Action.RoomLoaded", async () => {
jest.spyOn(defaultDispatcher, "dispatch");
await mountRoomView();
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
});
describe("when there is a RoomView", () => {
const widget1Id = "widget1";
const widget2Id = "widget2";
@ -444,7 +697,7 @@ describe("RoomView", () => {
skey: id,
ts,
});
room.addLiveEvents([widgetEvent], { addToState: true });
room.addLiveEvents([widgetEvent], { addToState: false });
room.currentState.setStateEvents([widgetEvent]);
cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null);
await flushPromises();
@ -509,181 +762,4 @@ describe("RoomView", () => {
});
});
});
it("should show error view if failed to look up room alias", async () => {
const { asFragment, findByText } = await renderRoomView(false);
defaultDispatcher.dispatch<ViewRoomErrorPayload>({
action: Action.ViewRoomError,
room_alias: "#addy:server",
room_id: null,
err: new MatrixError({ errcode: "M_NOT_FOUND" }),
});
await emitPromise(stores.roomViewStore, UPDATE_EVENT);
await findByText("Are you sure you're at the right place?");
expect(asFragment()).toMatchSnapshot();
});
describe("knock rooms", () => {
const client = createTestClient();
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
jest.spyOn(dis, "dispatch");
});
it("allows to request to join", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "knockRoom").mockResolvedValue({ room_id: room.roomId });
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
await untilDispatch(Action.SubmitAskToJoin, dis);
expect(dis.dispatch).toHaveBeenCalledWith({
action: "submit_ask_to_join",
roomId: room.roomId,
opts: { reason: undefined },
});
});
it("allows to cancel a join request", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "leave").mockResolvedValue({});
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Knock);
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
await untilDispatch(Action.CancelAskToJoin, dis);
expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId });
});
});
it("should close search results when edit is clicked", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<_RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
// @ts-ignore - triggering a search organically is a lot of work
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.Room,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
});
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await prom;
});
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
rooms.set(room2.roomId, room2);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<_RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
// @ts-ignore - triggering a search organically is a lot of work
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.All,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room2.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
});
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = untilDispatch(Action.ViewRoom, dis);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
});
it("fires Action.RoomLoaded", async () => {
jest.spyOn(dis, "dispatch");
await mountRoomView();
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
});
});

View file

@ -215,34 +215,33 @@ describe("ThreadPanel", () => {
myThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true });
myThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true });
let events: EventData[] = [];
const renderResult = render(<TestThreadPanel />);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(3);
const events = findEvents(renderResult.container);
expect(events).toHaveLength(3);
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
});
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
toggleThreadFilter(renderResult.container, ThreadFilterType.My);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(2);
const events = findEvents(renderResult.container);
expect(events).toHaveLength(2);
expect(events[0]).toEqual(toEventData(mixedThread.rootEvent));
expect(events[1]).toEqual(toEventData(ownThread.rootEvent));
});
expect(events[0]).toEqual(toEventData(mixedThread.rootEvent));
expect(events[1]).toEqual(toEventData(ownThread.rootEvent));
toggleThreadFilter(renderResult.container, ThreadFilterType.All);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(3);
const events = findEvents(renderResult.container);
expect(events).toHaveLength(3);
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
});
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
});
it("correctly filters Thread List with a single, unparticipated thread", async () => {
@ -261,28 +260,27 @@ describe("ThreadPanel", () => {
const [allThreads] = room.threadsTimelineSets;
allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true });
let events: EventData[] = [];
const renderResult = render(<TestThreadPanel />);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(1);
const events = findEvents(renderResult.container);
expect(events).toHaveLength(1);
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
});
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
toggleThreadFilter(renderResult.container, ThreadFilterType.My);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(0);
const events = findEvents(renderResult.container);
expect(events).toHaveLength(0);
});
toggleThreadFilter(renderResult.container, ThreadFilterType.All);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(1);
const events = findEvents(renderResult.container);
expect(events).toHaveLength(1);
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
});
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
});
});
});

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render, waitFor, screen } from "jest-matrix-react";
import { render, waitFor, screen, act, cleanup } from "jest-matrix-react";
import {
ReceiptType,
EventTimelineSet,
@ -28,7 +28,7 @@ import {
ThreadFilterType,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import React, { createRef } from "react";
import React from "react";
import { Mocked, mocked } from "jest-mock";
import { forEachRight } from "lodash";
@ -180,7 +180,7 @@ describe("TimelinePanel", () => {
const roomId = "#room:example.com";
let room: Room;
let timelineSet: EventTimelineSet;
let timelinePanel: TimelinePanel;
let timelinePanel: TimelinePanel | null = null;
const ev1 = new MatrixEvent({
event_id: "ev1",
@ -199,17 +199,16 @@ describe("TimelinePanel", () => {
});
const renderTimelinePanel = async (): Promise<void> => {
const ref = createRef<TimelinePanel>();
render(
<TimelinePanel
timelineSet={timelineSet}
manageReadMarkers={true}
manageReadReceipts={true}
ref={ref}
ref={(ref) => (timelinePanel = ref)}
/>,
);
await flushPromises();
timelinePanel = ref.current!;
await waitFor(() => expect(timelinePanel).toBeTruthy());
};
const setUpTimelineSet = (threadRoot?: MatrixEvent) => {
@ -234,8 +233,9 @@ describe("TimelinePanel", () => {
room = new Room(roomId, client, userId, { pendingEventOrdering: PendingEventOrdering.Detached });
});
afterEach(() => {
afterEach(async () => {
TimelinePanel.roomReadMarkerTsMap = {};
cleanup();
});
it("when there is no event, it should not send any receipt", async () => {
@ -260,7 +260,6 @@ describe("TimelinePanel", () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, { addToState: true });
await flushPromises();
// @ts-ignore
await timelinePanel.sendReadReceipts();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
@ -278,7 +277,7 @@ describe("TimelinePanel", () => {
client.setRoomReadMarkers.mockClear();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker();
await act(() => timelinePanel.updateReadMarker());
});
it("should not send receipts again", () => {
@ -293,7 +292,7 @@ describe("TimelinePanel", () => {
// setup, timelineSet is not actually the timelineSet of the room.
await room.addLiveEvents([ev2], { addToState: true });
room.addEphemeralEvents([newReceipt(ev2.getId()!, userId, 222, 200)]);
await timelinePanel.forgetReadMarker();
await timelinePanel!.forgetReadMarker();
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev2.getId());
});
});
@ -317,7 +316,7 @@ describe("TimelinePanel", () => {
it("should send a fully read marker and a private receipt", async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, { addToState: true });
act(() => timelineSet.addLiveEvent(ev1, { addToState: true }));
await flushPromises();
// @ts-ignore
@ -328,6 +327,7 @@ describe("TimelinePanel", () => {
// Expect the fully_read marker not to be send yet
expect(client.setRoomReadMarkers).not.toHaveBeenCalled();
await flushPromises();
client.sendReadReceipt.mockClear();
// @ts-ignore simulate user activity
@ -336,7 +336,7 @@ describe("TimelinePanel", () => {
// It should not send the receipt again.
expect(client.sendReadReceipt).not.toHaveBeenCalledWith(ev1, ReceiptType.ReadPrivate);
// Expect the fully_read marker to be sent after user activity.
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev1.getId());
await waitFor(() => expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev1.getId()));
});
});
});
@ -363,11 +363,11 @@ describe("TimelinePanel", () => {
it("should send receipts but no fully_read when reading the thread timeline", async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(threadEv1, { addToState: true });
act(() => timelineSet.addLiveEvent(threadEv1, { addToState: true }));
await flushPromises();
// @ts-ignore
await timelinePanel.sendReadReceipts();
await act(() => timelinePanel.sendReadReceipts());
// fully_read is not supported for threads per spec
expect(client.setRoomReadMarkers).not.toHaveBeenCalled();
@ -1032,7 +1032,7 @@ describe("TimelinePanel", () => {
await waitFor(() => expectEvents(container, [events[1]]));
});
defaultDispatcher.fire(Action.DumpDebugLogs);
act(() => defaultDispatcher.fire(Action.DumpDebugLogs));
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(expect.stringContaining("TimelinePanel(Room): Debugging info for roomId")),

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

@ -180,11 +180,11 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
Blog
</a>
<a
href="https://twitter.com/element_hq"
href="https://mastodon.matrix.org/@Element"
rel="noreferrer noopener"
target="_blank"
>
Twitter
Mastodon
</a>
<a
href="https://github.com/element-hq/element-web"
@ -357,11 +357,11 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo
Blog
</a>
<a
href="https://twitter.com/element_hq"
href="https://mastodon.matrix.org/@Element"
rel="noreferrer noopener"
target="_blank"
>
Twitter
Mastodon
</a>
<a
href="https://github.com/element-hq/element-web"

View file

@ -62,7 +62,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":rbc:"
aria-labelledby=":rg4:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -78,7 +78,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":rbh:"
aria-labelledby=":rg9:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -103,7 +103,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</button>
<button
aria-label="Room info"
aria-labelledby=":rbm:"
aria-labelledby=":rge:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -128,7 +128,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</button>
<button
aria-label="Threads"
aria-labelledby=":rbr:"
aria-labelledby=":rgj:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -157,7 +157,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
>
<div
aria-label="2 members"
aria-labelledby=":rc0:"
aria-labelledby=":rgo:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@ -280,7 +280,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":rca:"
aria-labelledby=":rh2:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -296,7 +296,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":rcf:"
aria-labelledby=":rh7:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -321,7 +321,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</button>
<button
aria-label="Room info"
aria-labelledby=":rck:"
aria-labelledby=":rhc:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -346,7 +346,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</button>
<button
aria-label="Threads"
aria-labelledby=":rcp:"
aria-labelledby=":rhh:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -375,7 +375,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
>
<div
aria-label="2 members"
aria-labelledby=":rcu:"
aria-labelledby=":rhm:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@ -583,7 +583,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":r70:"
aria-labelledby=":rbo:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -599,7 +599,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":r75:"
aria-labelledby=":rbt:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -624,7 +624,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</button>
<button
aria-label="Room info"
aria-labelledby=":r7a:"
aria-labelledby=":rc2:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -649,7 +649,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</button>
<button
aria-label="Threads"
aria-labelledby=":r7f:"
aria-labelledby=":rc7:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -678,7 +678,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
>
<div
aria-label="2 members"
aria-labelledby=":r7k:"
aria-labelledby=":rcc:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@ -963,7 +963,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":r96:"
aria-labelledby=":rdu:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -979,7 +979,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":r9b:"
aria-labelledby=":re3:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -1004,7 +1004,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</button>
<button
aria-label="Room info"
aria-labelledby=":r9g:"
aria-labelledby=":re8:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -1029,7 +1029,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</button>
<button
aria-label="Threads"
aria-labelledby=":r9l:"
aria-labelledby=":red:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -1058,7 +1058,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
>
<div
aria-label="2 members"
aria-labelledby=":r9q:"
aria-labelledby=":rei:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@ -1276,6 +1276,571 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</div>
`;
exports[`RoomView should not display the timeline when the room encryption is loading 1`] = `
<DocumentFragment>
<div
class="mx_RoomView"
>
<canvas
aria-hidden="true"
height="768"
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
width="0"
/>
<div
class="mx_MainSplit"
>
<div
class="mx_RoomView_body mx_MainSplit_timeline"
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<button
aria-label="Open room settings"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="button"
style="--cpd-avatar-size: 40px;"
tabindex="-1"
>
!
</button>
<button
aria-label="Room info"
class="mx_RoomHeader_infoWrapper"
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
style="--mx-box-flex: 1;"
>
<div
aria-level="1"
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
dir="auto"
role="heading"
>
<span
class="mx_RoomHeader_truncated mx_lineClamp"
>
!5:example.org
</span>
</div>
</div>
</button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
aria-disabled="true"
aria-label="There's no one here to call"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
aria-labelledby=":r2c:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
/>
</svg>
</div>
</button>
<button
aria-disabled="true"
aria-label="There's no one here to call"
aria-labelledby=":r2h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
/>
</svg>
</div>
</button>
<button
aria-label="Room info"
aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
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 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.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 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.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>
</button>
<button
aria-label="Threads"
aria-labelledby=":r2r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
</div>
<div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
>
<div
aria-label="0 members"
aria-labelledby=":r30:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_mcap2_111"
/>
0
</div>
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
<div />
</div>
<main
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
/>
<div
aria-label="Room status bar"
class="mx_RoomView_statusArea"
role="region"
>
<div
class="mx_RoomView_statusAreaBox"
>
<div
class="mx_RoomView_statusAreaBox_line"
/>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`RoomView should not display the timeline when the room encryption is loading 2`] = `
<DocumentFragment>
<div
class="mx_RoomView"
>
<canvas
aria-hidden="true"
height="768"
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
width="0"
/>
<div
class="mx_MainSplit"
>
<div
class="mx_RoomView_body mx_MainSplit_timeline"
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<button
aria-label="Open room settings"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="button"
style="--cpd-avatar-size: 40px;"
tabindex="-1"
>
!
</button>
<button
aria-label="Room info"
class="mx_RoomHeader_infoWrapper"
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
style="--mx-box-flex: 1;"
>
<div
aria-level="1"
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
dir="auto"
role="heading"
>
<span
class="mx_RoomHeader_truncated mx_lineClamp"
>
!5:example.org
</span>
</div>
</div>
</button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
aria-disabled="true"
aria-label="There's no one here to call"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
aria-labelledby=":r2c:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
/>
</svg>
</div>
</button>
<button
aria-disabled="true"
aria-label="There's no one here to call"
aria-labelledby=":r2h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
/>
</svg>
</div>
</button>
<button
aria-label="Room info"
aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
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 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.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 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.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>
</button>
<button
aria-label="Threads"
aria-labelledby=":r2r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
</div>
<div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
>
<div
aria-label="0 members"
aria-labelledby=":r30:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_mcap2_111"
/>
0
</div>
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
<div />
</div>
<main
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
class="mx_RoomView_messageListWrapper"
>
<ol
aria-live="polite"
class="mx_RoomView_MessageList"
style="height: 400px;"
/>
</div>
</div>
</main>
<div
aria-label="Room status bar"
class="mx_RoomView_statusArea"
role="region"
>
<div
class="mx_RoomView_statusAreaBox"
>
<div
class="mx_RoomView_statusAreaBox_line"
/>
</div>
</div>
<div
aria-label="Message composer"
class="mx_MessageComposer mx_MessageComposer_e2eStatus"
role="region"
>
<div
class="mx_MessageComposer_wrapper"
>
<div
class="mx_MessageComposer_row"
>
<div
class="mx_MessageComposer_e2eIconWrapper"
>
<span
tabindex="0"
>
<div
aria-labelledby=":r3e:"
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
/>
</span>
</div>
<div
class="mx_SendMessageComposer"
>
<div
class="mx_BasicMessageComposer"
>
<div
aria-label="Formatting"
class="mx_MessageComposerFormatBar"
role="toolbar"
>
<button
aria-label="Bold"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
role="button"
tabindex="0"
type="button"
/>
<button
aria-label="Italics"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Strikethrough"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Code block"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Quote"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Insert link"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
role="button"
tabindex="-1"
type="button"
/>
</div>
<div
aria-autocomplete="list"
aria-disabled="false"
aria-haspopup="listbox"
aria-label="Send an encrypted message…"
aria-multiline="true"
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
contenteditable="true"
data-testid="basicmessagecomposer"
dir="auto"
role="textbox"
style="--placeholder: 'Send\\ an\\ encrypted\\ message…';"
tabindex="0"
translate="no"
>
<div>
<br />
</div>
</div>
</div>
</div>
<div
class="mx_MessageComposer_actions"
>
<div
aria-label="Emoji"
class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"
role="button"
tabindex="0"
/>
<div
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"
role="button"
tabindex="0"
/>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
role="button"
tabindex="0"
/>
<input
multiple=""
style="display: none;"
type="file"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`RoomView should show error view if failed to look up room alias 1`] = `
<DocumentFragment>
<div
@ -1332,7 +1897,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
aria-label="Open room settings"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-color="5"
data-testid="avatar-img"
data-type="round"
role="button"
@ -1359,7 +1924,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
<span
class="mx_RoomHeader_truncated mx_lineClamp"
>
!10:example.org
!12:example.org
</span>
</div>
</div>
@ -1370,7 +1935,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<button
aria-label="Room info"
aria-labelledby=":r2k:"
aria-labelledby=":r7c:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -1395,7 +1960,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Chat"
aria-labelledby=":r2p:"
aria-labelledby=":r7h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -1420,7 +1985,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby=":r2u:"
aria-labelledby=":r7m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@ -1449,7 +2014,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<div
aria-label="0 members"
aria-labelledby=":r33:"
aria-labelledby=":r7r:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@ -1487,7 +2052,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</p>
</div>
<button
aria-labelledby=":r3c:"
aria-labelledby=":r84:"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"

View file

@ -8,19 +8,13 @@ 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, waitFor } from "jest-matrix-react";
import { render, RenderResult, screen, waitFor, cleanup } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
import ForgotPassword from "../../../../../src/components/structures/auth/ForgotPassword";
import { ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig";
import {
clearAllModals,
filterConsole,
flushPromisesWithFakeTimers,
stubClient,
waitEnoughCyclesForModal,
} from "../../../../test-utils";
import { clearAllModals, filterConsole, stubClient, waitEnoughCyclesForModal } from "../../../../test-utils";
import AutoDiscoveryUtils from "../../../../../src/utils/AutoDiscoveryUtils";
jest.mock("matrix-js-sdk/src/matrix", () => ({
@ -39,11 +33,7 @@ describe("<ForgotPassword>", () => {
let renderResult: RenderResult;
const typeIntoField = async (label: string, value: string): Promise<void> => {
await act(async () => {
await userEvent.type(screen.getByLabelText(label), value, { delay: null });
// the message is shown after some time
jest.advanceTimersByTime(500);
});
await userEvent.type(screen.getByLabelText(label), value, { delay: null });
};
const click = async (element: Element): Promise<void> => {
@ -78,14 +68,7 @@ describe("<ForgotPassword>", () => {
afterEach(async () => {
// clean up modals
await clearAllModals();
});
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
cleanup();
});
describe("when starting a password reset flow", () => {
@ -128,13 +111,16 @@ describe("<ForgotPassword>", () => {
await typeIntoField("Email address", "not en email");
});
it("should show a message about the wrong format", () => {
expect(screen.getByText("The email address doesn't appear to be valid.")).toBeInTheDocument();
it("should show a message about the wrong format", async () => {
await expect(
screen.findByText("The email address doesn't appear to be valid."),
).resolves.toBeInTheDocument();
});
});
describe("and submitting an unknown email", () => {
beforeEach(async () => {
mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockResolvedValue(serverConfig);
await typeIntoField("Email address", testEmail);
mocked(client).requestPasswordEmailToken.mockRejectedValue({
errcode: "M_THREEPID_NOT_FOUND",
@ -142,8 +128,8 @@ describe("<ForgotPassword>", () => {
await click(screen.getByText("Send email"));
});
it("should show an email not found message", () => {
expect(screen.getByText("This email address was not found")).toBeInTheDocument();
it("should show an email not found message", async () => {
await expect(screen.findByText("This email address was not found")).resolves.toBeInTheDocument();
});
});
@ -156,13 +142,12 @@ describe("<ForgotPassword>", () => {
await click(screen.getByText("Send email"));
});
it("should show an info about that", () => {
expect(
screen.getByText(
"Cannot reach homeserver: " +
"Ensure you have a stable internet connection, or get in touch with the server admin",
it("should show an info about that", async () => {
await expect(
screen.findByText(
"Cannot reach homeserver: Ensure you have a stable internet connection, or get in touch with the server admin",
),
).toBeInTheDocument();
).resolves.toBeInTheDocument();
});
});
@ -178,8 +163,8 @@ describe("<ForgotPassword>", () => {
await click(screen.getByText("Send email"));
});
it("should show the server error", () => {
expect(screen.queryByText("server down")).toBeInTheDocument();
it("should show the server error", async () => {
await expect(screen.findByText("server down")).resolves.toBeInTheDocument();
});
});
@ -215,8 +200,6 @@ describe("<ForgotPassword>", () => {
describe("and clicking »Resend«", () => {
beforeEach(async () => {
await click(screen.getByText("Resend"));
// the message is shown after some time
jest.advanceTimersByTime(500);
});
it("should should resend the mail and show the tooltip", () => {
@ -246,8 +229,10 @@ describe("<ForgotPassword>", () => {
await typeIntoField("Confirm new password", testPassword + "asd");
});
it("should show an info about that", () => {
expect(screen.getByText("New passwords must match each other.")).toBeInTheDocument();
it("should show an info about that", async () => {
await expect(
screen.findByText("New passwords must match each other."),
).resolves.toBeInTheDocument();
});
});
@ -284,7 +269,7 @@ describe("<ForgotPassword>", () => {
await click(screen.getByText("Reset password"));
});
it("should send the new password (once)", () => {
it("should send the new password (once)", async () => {
expect(client.setPassword).toHaveBeenCalledWith(
{
type: "m.login.email.identity",
@ -297,19 +282,15 @@ describe("<ForgotPassword>", () => {
false,
);
// be sure that the next attempt to set the password would have been sent
jest.advanceTimersByTime(3000);
// it should not retry to set the password
expect(client.setPassword).toHaveBeenCalledTimes(1);
await waitFor(() => expect(client.setPassword).toHaveBeenCalledTimes(1));
});
});
describe("and submitting it", () => {
beforeEach(async () => {
await click(screen.getByText("Reset password"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
await waitEnoughCyclesForModal();
});
it("should send the new password and show the click validation link dialog", async () => {
@ -367,23 +348,22 @@ describe("<ForgotPassword>", () => {
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
});
});
});
describe("and validating the link from the mail", () => {
beforeEach(async () => {
mocked(client.setPassword).mockResolvedValue({});
// be sure the next set password attempt was sent
jest.advanceTimersByTime(3000);
// quad flush promises for the modal to disappear
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
});
describe("and validating the link from the mail", () => {
beforeEach(async () => {
mocked(client.setPassword).mockResolvedValue({});
await click(screen.getByText("Reset password"));
// flush promises for the modal to disappear
await waitEnoughCyclesForModal();
await waitEnoughCyclesForModal();
});
it("should display the confirm reset view and now show the dialog", () => {
expect(screen.queryByText("Your password has been reset.")).toBeInTheDocument();
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
});
it("should display the confirm reset view and now show the dialog", async () => {
await expect(
screen.findByText("Your password has been reset."),
).resolves.toBeInTheDocument();
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
});
});
@ -391,9 +371,6 @@ describe("<ForgotPassword>", () => {
beforeEach(async () => {
await click(screen.getByText("Sign out of all devices"));
await click(screen.getByText("Reset password"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
});
it("should show the sign out warning dialog", async () => {

View file

@ -14,11 +14,11 @@ exports[`<AuthFooter /> should match snapshot 1`] = `
Blog
</a>
<a
href="https://twitter.com/element_hq"
href="https://mastodon.matrix.org/@Element"
rel="noreferrer noopener"
target="_blank"
>
Twitter
Mastodon
</a>
<a
href="https://github.com/element-hq/element-web"

View file

@ -30,11 +30,11 @@ exports[`<AuthPage /> should match snapshot 1`] = `
Blog
</a>
<a
href="https://twitter.com/element_hq"
href="https://mastodon.matrix.org/@Element"
rel="noreferrer noopener"
target="_blank"
>
Twitter
Mastodon
</a>
<a
href="https://github.com/element-hq/element-web"

View file

@ -8,9 +8,18 @@ exports[`<BeaconViewDialog /> renders a fallback when there are no locations 1`]
<div
class="mx_MapFallback_bg"
/>
<div
<svg
class="mx_MapFallback_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
<span
class="mx_BeaconViewDialog_mapFallbackMessage"
>

View file

@ -150,7 +150,7 @@ describe("RoomGeneralContextMenu", () => {
await sleep(0);
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "m.marked_unread", {
unread: true,
});
expect(onFinished).toHaveBeenCalled();

View file

@ -122,4 +122,34 @@ describe("AccessSecretStorageDialog", () => {
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
});
it("Can reset secret storage", async () => {
jest.spyOn(mockClient.secretStorage, "checkKey").mockResolvedValue(true);
const onFinished = jest.fn();
const checkPrivateKey = jest.fn().mockResolvedValue(true);
renderComponent({ onFinished, checkPrivateKey });
await userEvent.click(screen.getByText("Reset all"), { delay: null });
// It will prompt the user to confirm resetting
expect(screen.getByText("Reset everything")).toBeInTheDocument();
await userEvent.click(screen.getByText("Reset"), { delay: null });
// Then it will prompt the user to create a key/passphrase
await screen.findByText("Set up Secure Backup");
document.execCommand = jest.fn().mockReturnValue(true);
jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({
privateKey: new Uint8Array(),
encodedPrivateKey: securityKey,
});
screen.getByRole("button", { name: "Continue" }).click();
await screen.findByText(/Save your Security Key/);
screen.getByRole("button", { name: "Copy" }).click();
await screen.findByText("Copied!");
screen.getByRole("button", { name: "Continue" }).click();
await screen.findByText("Secure Backup successful");
});
});

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";
@ -22,7 +22,6 @@ describe("LogoutDialog", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsCrypto(),
getKeyBackupVersion: jest.fn(),
});
mockCrypto = mocked(mockClient.getCrypto()!);
@ -50,23 +49,26 @@ describe("LogoutDialog", () => {
});
it("Prompts user to connect backup if there is a backup on the server", async () => {
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
const rendered = renderComponent();
await rendered.findByText("Connect this session to Key Backup");
expect(rendered.container).toMatchSnapshot();
});
it("Prompts user to set up backup if there is no backup on the server", async () => {
mockClient.getKeyBackupVersion.mockResolvedValue(null);
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
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", () => {
filterConsole("Unable to fetch key backup status");
it("prompts user to set up backup", async () => {
mockClient.getKeyBackupVersion.mockImplementation(async () => {
mockCrypto.getKeyBackupInfo.mockImplementation(async () => {
throw new Error("beep");
});
const rendered = renderComponent();

View file

@ -239,7 +239,7 @@ describe("Spotlight Dialog", () => {
});
it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => {
render(<SpotlightDialog onFinished={() => null} />, { legacyRoot: false });
render(<SpotlightDialog onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(true);

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

@ -13,7 +13,7 @@ import { mocked, MockedObject } from "jest-mock";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import { filterConsole, stubClient } from "../../../../../test-utils";
import { filterConsole, flushPromises, stubClient } from "../../../../../test-utils";
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
describe("CreateSecretStorageDialog", () => {
@ -77,7 +77,7 @@ describe("CreateSecretStorageDialog", () => {
filterConsole("Error fetching backup data from server");
it("shows an error", async () => {
mockClient.getKeyBackupVersion.mockImplementation(async () => {
jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
throw new Error("bleh bleh");
});
@ -92,9 +92,44 @@ describe("CreateSecretStorageDialog", () => {
expect(result.container).toMatchSnapshot();
// Now we can get the backup and we retry
mockClient.getKeyBackupVersion.mockRestore();
jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockRestore();
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
await screen.findByText("Your keys are now being backed up from this device.");
});
});
it("resets keys in the right order when resetting secret storage and cross-signing", async () => {
const result = renderComponent({ forceReset: true, resetCrossSigning: true });
await result.findByText(/Set up Secure Backup/);
jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({
privateKey: new Uint8Array(),
encodedPrivateKey: "abcd efgh ijkl",
});
result.getByRole("button", { name: "Continue" }).click();
await result.findByText(/Save your Security Key/);
result.getByRole("button", { name: "Copy" }).click();
// Resetting should reset secret storage, cross signing, and key
// backup. We make sure that all three are reset, and done in the
// right order.
const resetFunctionCallLog: string[] = [];
jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockImplementation(async () => {
resetFunctionCallLog.push("bootstrapSecretStorage");
});
jest.spyOn(mockClient.getCrypto()!, "bootstrapCrossSigning").mockImplementation(async () => {
resetFunctionCallLog.push("bootstrapCrossSigning");
});
jest.spyOn(mockClient.getCrypto()!, "resetKeyBackup").mockImplementation(async () => {
resetFunctionCallLog.push("resetKeyBackup");
});
await flushPromises();
result.getByRole("button", { name: "Continue" }).click();
await result.findByText("Your keys are now being backed up from this device.");
expect(resetFunctionCallLog).toEqual(["bootstrapSecretStorage", "bootstrapCrossSigning", "resetKeyBackup"]);
});
});

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { screen, fireEvent, render, waitFor } from "jest-matrix-react";
import { screen, fireEvent, render, waitFor, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix";
@ -23,12 +23,12 @@ describe("ExportE2eKeysDialog", () => {
expect(asFragment()).toMatchSnapshot();
});
it("should have disabled submit button initially", () => {
it("should have disabled submit button initially", async () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.click(container.querySelector("[type=submit]")!);
expect(screen.getByText("Enter passphrase")).toBeInTheDocument();
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
expect(screen.getByLabelText("Enter passphrase")).toBeInTheDocument();
});
it("should complain about weak passphrases", async () => {
@ -38,7 +38,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
const input = screen.getByLabelText("Enter passphrase");
await userEvent.type(input, "password");
fireEvent.click(container.querySelector("[type=submit]")!);
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
await expect(screen.findByText("This is a top-10 common password")).resolves.toBeInTheDocument();
});
@ -49,7 +49,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), "ThisIsAMoreSecurePW123$$");
await userEvent.type(screen.getByLabelText("Confirm passphrase"), "ThisIsAMoreSecurePW124$$");
fireEvent.click(container.querySelector("[type=submit]")!);
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
await expect(screen.findByText("Passphrases must match")).resolves.toBeInTheDocument();
});
@ -74,7 +74,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={jest.fn()} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), passphrase);
await userEvent.type(screen.getByLabelText("Confirm passphrase"), passphrase);
fireEvent.click(container.querySelector("[type=submit]")!);
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
// Then it exports keys and encrypts them
await waitFor(() => expect(exportRoomKeysAsJson).toHaveBeenCalled());

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.getCrypto()!, "getKeyBackupInfo").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.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
version: "1",
auth_data: {
private_key_salt: "salt",
private_key_iterations: 1,
},
} as KeyBackupInfo);
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackup")
// Reject when trying to restore from cache
.mockRejectedValue(new Error("key backup not found"));
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackupWithPassphrase").mockResolvedValue(
keyBackupRestoreResult,
);
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Enter Security Phrase")).toBeInTheDocument());
// Not role for password https://github.com/w3c/aria/issues/935
await userEvent.type(screen.getByTestId("passphraseInput"), "my passphrase");
await userEvent.click(screen.getByRole("button", { name: "Next" }));
await waitFor(() => expect(screen.getByText("Successfully restored 1 keys")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
});

View file

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

View file

@ -7,13 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { jest } from "@jest/globals";
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk";
import { act, render, RenderResult } from "jest-matrix-react";
import { act, render, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { SpiedFunction } from "jest-mock";
import {
ApprovalOpts,
WidgetInfo,
@ -31,7 +29,6 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore";
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
import AppTile from "../../../../../src/components/views/elements/AppTile";
@ -61,16 +58,6 @@ describe("AppTile", () => {
let app1: IApp;
let app2: IApp;
const waitForRps = (roomId: string) =>
new Promise<void>((resolve) => {
const update = () => {
if (RightPanelStore.instance.currentCardForRoom(roomId).phase !== RightPanelPhases.Widget) return;
RightPanelStore.instance.off(UPDATE_EVENT, update);
resolve();
};
RightPanelStore.instance.on(UPDATE_EVENT, update);
});
beforeAll(async () => {
stubClient();
cli = MatrixClientPeg.safeGet();
@ -162,29 +149,28 @@ describe("AppTile", () => {
/>
</MatrixClientContext.Provider>,
);
// Wait for RPS room 1 updates to fire
const rpsUpdated = waitForRps("r1");
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await rpsUpdated;
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
}),
);
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
await expect(renderResult.findByText("Example 1")).resolves.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
const { container, asFragment } = renderResult;
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
const { asFragment } = renderResult;
expect(asFragment()).toMatchSnapshot();
// We want to verify that as we change to room 2, we should close the
// right panel and destroy the widget.
// Switch to room 2
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
});
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
}),
);
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
@ -235,16 +221,17 @@ describe("AppTile", () => {
/>
</MatrixClientContext.Provider>,
);
// Wait for RPS room 1 updates to fire
const rpsUpdated1 = waitForRps("r1");
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await rpsUpdated1;
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
}),
);
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
await waitFor(() => {
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
});
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name === "RightPanel.phases") {
@ -265,13 +252,13 @@ describe("AppTile", () => {
}
return realGetValue(name, roomId);
});
// Wait for RPS room 2 updates to fire
const rpsUpdated2 = waitForRps("r2");
// Switch to room 2
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
});
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
}),
);
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
<RightPanel
@ -281,10 +268,11 @@ describe("AppTile", () => {
/>
</MatrixClientContext.Provider>,
);
await rpsUpdated2;
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true);
await waitFor(() => {
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true);
});
});
it("preserves non-persisted widget on container move", async () => {
@ -345,9 +333,9 @@ 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(() => {
beforeEach(async () => {
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
@ -355,12 +343,12 @@ describe("AppTile", () => {
);
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", () => {
const { container, asFragment } = renderResult;
const { asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy(); // Assert that the spinner is gone
expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
});
@ -461,18 +449,19 @@ describe("AppTile", () => {
describe("for a persistent app", () => {
let renderResult: RenderResult;
beforeEach(() => {
beforeEach(async () => {
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", () => {
const { container, asFragment } = renderResult;
it("should render", async () => {
const { asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,40 @@
/*
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 } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import MiniAvatarUploader from "../../../../../src/components/views/elements/MiniAvatarUploader.tsx";
import { stubClient, withClientContextRenderOptions } from "../../../../test-utils";
const BASE64_GIF = "R0lGODlhAQABAAAAACw=";
const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", {
type: "image/gif",
});
describe("<MiniAvatarUploader />", () => {
it("calls setAvatarUrl when a file is uploaded", async () => {
const cli = stubClient();
mocked(cli.uploadContent).mockResolvedValue({ content_uri: "mxc://example.com/1234" });
const setAvatarUrl = jest.fn();
const user = userEvent.setup();
const { container, findByText } = render(
<MiniAvatarUploader hasAvatar={false} noAvatarLabel="Upload" setAvatarUrl={setAvatarUrl} isUserAvatar />,
withClientContextRenderOptions(cli),
);
await findByText("Upload");
await user.upload(container.querySelector("input")!, AVATAR_FILE);
expect(cli.uploadContent).toHaveBeenCalledWith(AVATAR_FILE);
expect(setAvatarUrl).toHaveBeenCalledWith("mxc://example.com/1234");
});
});

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, render, RenderResult, screen } from "jest-matrix-react";
import { render, RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked, Mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
@ -214,9 +214,7 @@ describe("<Pill>", () => {
});
// wait for profile query via API
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(renderResult.asFragment()).toMatchSnapshot();
});
@ -228,9 +226,7 @@ describe("<Pill>", () => {
});
// wait for profile query via API
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(renderResult.asFragment()).toMatchSnapshot();
});

View file

@ -60,29 +60,9 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] =
id="1"
>
<div
class="mx_AppTileBody mx_AppTileBody--large"
class="mx_AppTile_persistedWrapper"
>
<div
class="mx_AppTileBody_fadeInSpinner"
>
<div
class="mx_Spinner"
>
<div
class="mx_Spinner_Msg"
>
Loading…
</div>
 
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div />
</div>
</div>
</div>
@ -356,8 +336,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

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { createRef } from "react";
import { render, waitFor } from "jest-matrix-react";
import { render, waitFor, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import EmojiPicker from "../../../../../src/components/views/emojipicker/EmojiPicker";
@ -27,12 +27,12 @@ describe("EmojiPicker", function () {
// Apply a filter and assert that the HTML has changed
//@ts-ignore private access
ref.current!.onChangeFilter("test");
act(() => ref.current!.onChangeFilter("test"));
expect(beforeHtml).not.toEqual(container.innerHTML);
// Clear the filter and assert that the HTML matches what it was before filtering
//@ts-ignore private access
ref.current!.onChangeFilter("");
act(() => ref.current!.onChangeFilter(""));
await waitFor(() => expect(beforeHtml).toEqual(container.innerHTML));
});
@ -40,7 +40,7 @@ describe("EmojiPicker", function () {
const ep = new EmojiPicker({ onChoose: (str: string) => false, onFinished: jest.fn() });
//@ts-ignore private access
ep.onChangeFilter("heart");
act(() => ep.onChangeFilter("heart"));
//@ts-ignore private access
expect(ep.memoizedDataByCategory["people"][0].shortcodes[0]).toEqual("heart");

View file

@ -139,7 +139,7 @@ describe("<LocationShareMenu />", () => {
const [, onGeolocateCallback] = mocked(mockGeolocate.on).mock.calls.find(([event]) => event === "geolocate")!;
// set the location
onGeolocateCallback(position);
act(() => onGeolocateCallback(position));
};
const setLocationClick = () => {
@ -151,7 +151,7 @@ describe("<LocationShareMenu />", () => {
lngLat: { lng: position.coords.longitude, lat: position.coords.latitude },
} as unknown as maplibregl.MapMouseEvent;
// set the location
onMapClickCallback(event);
act(() => onMapClickCallback(event));
};
const shareTypeLabels: Record<LocationShareType, string> = {

View file

@ -13,9 +13,18 @@ exports[`<LocationViewDialog /> renders map correctly 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>
@ -23,7 +32,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

@ -9,9 +9,18 @@ exports[`<Marker /> renders with location icon when no room member 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</DocumentFragment>

View file

@ -9,9 +9,18 @@ exports[`<SmartMarker /> creates a marker on mount 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>
@ -27,9 +36,18 @@ exports[`<SmartMarker /> removes marker on unmount 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>

View file

@ -91,6 +91,12 @@ describe("DateSeparator", () => {
expect(getComponent({ ts, forExport: false }).container.textContent).toEqual(result);
});
it("renders invalid date separator correctly", () => {
const ts = new Date(-8640000000000004).getTime();
const { asFragment } = getComponent({ ts });
expect(asFragment()).toMatchSnapshot();
});
describe("when forExport is true", () => {
it.each(testCases)("formats date in full when current time is %s", (_d, ts) => {
expect(getComponent({ ts, forExport: true }).container.textContent).toEqual(
@ -258,10 +264,12 @@ describe("DateSeparator", () => {
fireEvent.click(jumpToLastWeekButton);
// Expect error to be shown. We have to wait for the UI to transition.
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
await expect(screen.findByTestId("jump-to-date-error-content")).resolves.toBeInTheDocument();
// Expect an option to submit debug logs to be shown when a non-network error occurs
expect(await screen.findByTestId("jump-to-date-error-submit-debug-logs-button")).toBeInTheDocument();
await expect(
screen.findByTestId("jump-to-date-error-submit-debug-logs-button"),
).resolves.toBeInTheDocument();
});
[
@ -274,19 +282,20 @@ describe("DateSeparator", () => {
),
].forEach((fakeError) => {
it(`should show error dialog without submit debug logs option when networking error (${fakeError.name}) occurs`, async () => {
// Try to jump to "last week" but we want a network error to occur
mockClient.timestampToEvent.mockRejectedValue(fakeError);
// Render the component
getComponent();
// Open the jump to date context menu
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
// Try to jump to "last week" but we want a network error to occur
mockClient.timestampToEvent.mockRejectedValue(fakeError);
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
fireEvent.click(jumpToLastWeekButton);
// Expect error to be shown. We have to wait for the UI to transition.
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
await expect(screen.findByTestId("jump-to-date-error-content")).resolves.toBeInTheDocument();
// The submit debug logs option should *NOT* be shown for network errors.
//

View file

@ -10,6 +10,7 @@ import React from "react";
import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import EncryptionEvent from "../../../../../src/components/views/messages/EncryptionEvent";
import { createTestClient, mkMessage } from "../../../../test-utils";
@ -26,9 +27,9 @@ const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => {
);
};
const checkTexts = (title: string, subTitle: string) => {
screen.getByText(title);
screen.getByText(subTitle);
const checkTexts = async (title: string, subTitle: string) => {
await screen.findByText(title);
await screen.findByText(subTitle);
};
describe("EncryptionEvent", () => {
@ -55,17 +56,19 @@ describe("EncryptionEvent", () => {
describe("for an encrypted room", () => {
beforeEach(() => {
event.event.content!.algorithm = algorithm;
mocked(client.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
const room = new Room(roomId, client, client.getUserId()!);
mocked(client.getRoom).mockReturnValue(room);
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
renderEncryptionEvent(client, event);
checkTexts(
"Encryption enabled",
"Messages in this room are end-to-end encrypted. " +
"When people join, you can verify them in their profile, just tap on their profile picture.",
await waitFor(() =>
checkTexts(
"Encryption enabled",
"Messages in this room are end-to-end encrypted. " +
"When people join, you can verify them in their profile, just tap on their profile picture.",
),
);
});
@ -76,9 +79,9 @@ describe("EncryptionEvent", () => {
});
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
renderEncryptionEvent(client, event);
checkTexts("Encryption enabled", "Some encryption parameters have been changed.");
await waitFor(() => checkTexts("Encryption enabled", "Some encryption parameters have been changed."));
});
});
@ -87,37 +90,39 @@ describe("EncryptionEvent", () => {
event.event.content!.algorithm = "unknown";
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
renderEncryptionEvent(client, event);
checkTexts("Encryption enabled", "Ignored attempt to disable encryption");
await waitFor(() => checkTexts("Encryption enabled", "Ignored attempt to disable encryption"));
});
});
});
describe("for an unencrypted room", () => {
beforeEach(() => {
mocked(client.isRoomEncrypted).mockReturnValue(false);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
renderEncryptionEvent(client, event);
});
it("should show the expected texts", () => {
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported.");
it("should show the expected texts", async () => {
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId);
await waitFor(() =>
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."),
);
});
});
describe("for an encrypted local room", () => {
beforeEach(() => {
event.event.content!.algorithm = algorithm;
mocked(client.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
const localRoom = new LocalRoom(roomId, client, client.getUserId()!);
mocked(client.getRoom).mockReturnValue(localRoom);
renderEncryptionEvent(client, event);
});
it("should show the expected texts", () => {
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
it("should show the expected texts", async () => {
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId);
await checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
});
});
});

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, waitFor } from "jest-matrix-react";
import { act, fireEvent, render, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import {
MatrixEvent,
Relations,
@ -226,7 +226,7 @@ describe("MPollBody", () => {
clickOption(renderResult, "pizza");
// When a new vote from me comes in
await room.processPollEvents([responseEvent("@me:example.com", "wings", 101)]);
await act(() => room.processPollEvents([responseEvent("@me:example.com", "wings", 101)]));
// Then the new vote is counted, not the old one
expect(votesCount(renderResult, "pizza")).toBe("0 votes");
@ -255,7 +255,7 @@ describe("MPollBody", () => {
clickOption(renderResult, "pizza");
// When a new vote from someone else comes in
await room.processPollEvents([responseEvent("@xx:example.com", "wings", 101)]);
await act(() => room.processPollEvents([responseEvent("@xx:example.com", "wings", 101)]));
// Then my vote is still for pizza
// NOTE: the new event does not affect the counts for other people -
@ -596,11 +596,13 @@ describe("MPollBody", () => {
];
const renderResult = await newMPollBody(votes, ends);
expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes");
expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes");
expect(endedVotesCount(renderResult, "italian")).toBe("0 votes");
expect(endedVotesCount(renderResult, "wings")).toBe('<div class="mx_PollOption_winnerIcon"></div>3 votes');
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes");
await waitFor(() => {
expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes");
expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes");
expect(endedVotesCount(renderResult, "italian")).toBe("0 votes");
expect(endedVotesCount(renderResult, "wings")).toBe('<div class="mx_PollOption_winnerIcon"></div>3 votes');
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes");
});
});
it("ignores votes that arrived after the first end poll event", async () => {
@ -890,12 +892,14 @@ async function newMPollBody(
room_id: "#myroom:example.com",
content: newPollStart(answers, undefined, disclosed),
});
const result = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents);
// flush promises from loading relations
const prom = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents);
if (waitForResponsesLoad) {
await flushPromises();
const result = await prom;
if (result.queryByTestId("spinner")) {
await waitForElementToBeRemoved(() => result.getByTestId("spinner"));
}
}
return result;
return prom;
}
function getMPollBodyPropsFromEvent(mxEvent: MatrixEvent): IBodyProps {

View file

@ -121,12 +121,13 @@ describe("<MPollEndBody />", () => {
describe("when poll start event does not exist in current timeline", () => {
it("fetches the related poll start event and displays a poll tile", async () => {
await setupRoomWithEventsTimeline(pollEndEvent);
const { container, getByTestId, getByRole } = getComponent();
const { container, getByTestId, getByRole, queryByRole } = getComponent();
// while fetching event, only icon is shown
expect(container).toMatchSnapshot();
await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument());
await waitFor(() => expect(queryByRole("progressbar")).not.toBeInTheDocument());
expect(mockClient.fetchRoomEvent).toHaveBeenCalledWith(roomId, pollStartEvent.getId());

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

@ -49,9 +49,18 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import { fireEvent, render } from "jest-matrix-react";
import { Filter, EventTimeline, Room, MatrixEvent, M_POLL_START } from "matrix-js-sdk/src/matrix";
import { PollHistory } from "../../../../../../src/components/views/polls/pollHistory/PollHistory";
@ -110,7 +110,7 @@ describe("<PollHistory />", () => {
expect(getByText("Loading polls")).toBeInTheDocument();
// flush filter creation request
await act(flushPromises);
await flushPromises();
expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS);
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true });
@ -140,7 +140,7 @@ describe("<PollHistory />", () => {
);
// flush filter creation request
await act(flushPromises);
await flushPromises();
// once per page
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
@ -175,7 +175,7 @@ describe("<PollHistory />", () => {
it("renders a no polls message when there are no active polls in the room", async () => {
const { getByText } = getComponent();
await act(flushPromises);
await flushPromises();
expect(getByText("There are no active polls in this room")).toBeTruthy();
});
@ -199,7 +199,7 @@ describe("<PollHistory />", () => {
.mockReturnValueOnce("test-pagination-token-3");
const { getByText } = getComponent();
await act(flushPromises);
await flushPromises();
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
@ -212,7 +212,7 @@ describe("<PollHistory />", () => {
// load more polls button still in UI, with loader
expect(getByText("Load more polls")).toMatchSnapshot();
await act(flushPromises);
await flushPromises();
// no more spinner
expect(getByText("Load more polls")).toMatchSnapshot();

View file

@ -91,7 +91,7 @@ exports[`<PollHistory /> renders a list of active polls when there are polls in
tabindex="0"
>
<div
aria-labelledby=":ra:"
aria-labelledby=":rc:"
class="mx_PollListItem_content"
>
<span>
@ -116,7 +116,7 @@ exports[`<PollHistory /> renders a list of active polls when there are polls in
tabindex="0"
>
<div
aria-labelledby=":rg:"
aria-labelledby=":rh:"
class="mx_PollListItem_content"
>
<span>

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

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, waitFor, cleanup, act, within } from "jest-matrix-react";
import { fireEvent, render, screen, cleanup, act, within, waitForElementToBeRemoved } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Mocked, mocked } from "jest-mock";
import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType, Device } from "matrix-js-sdk/src/matrix";
@ -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")),
@ -439,7 +439,7 @@ describe("<UserInfo />", () => {
it("renders a device list which can be expanded", async () => {
renderComponent();
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text
const devicesButton = screen.getByRole("button", { name: "1 session" });
@ -459,9 +459,9 @@ describe("<UserInfo />", () => {
verificationRequest,
room: mockRoom,
});
await act(flushPromises);
await flushPromises();
await waitFor(() => expect(screen.getByRole("button", { name: "Verify" })).toBeInTheDocument());
await expect(screen.findByRole("button", { name: "Verify" })).resolves.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
@ -490,7 +490,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "1 session" });
@ -538,7 +538,7 @@ describe("<UserInfo />", () => {
} as DeviceVerificationStatus);
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "1 verified session" });
@ -583,7 +583,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true));
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// the dehydrated device should be shown as an unverified device, which means
// there should now be a button with the device id ...
@ -618,7 +618,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "2 sessions" });
@ -653,20 +653,23 @@ describe("<UserInfo />", () => {
room: mockRoom,
});
await waitFor(() => expect(screen.getByRole("button", { name: "Deactivate user" })).toBeInTheDocument());
await expect(screen.findByRole("button", { name: "Deactivate user" })).resolves.toBeInTheDocument();
if (screen.queryAllByRole("progressbar").length) {
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
}
expect(container).toMatchSnapshot();
});
});
describe("with an encrypted room", () => {
beforeEach(() => {
mockClient.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
});
it("renders unverified user info", async () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
@ -677,7 +680,7 @@ describe("<UserInfo />", () => {
it("renders verified user info", async () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, false, false));
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
@ -768,7 +771,7 @@ describe("<DeviceItem />", () => {
it("with unverified user and device, displays button without a label", async () => {
renderComponent();
await act(flushPromises);
await flushPromises();
expect(screen.getByRole("button", { name: device.displayName! })).toBeInTheDocument();
expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument();
@ -776,7 +779,7 @@ describe("<DeviceItem />", () => {
it("with verified user only, displays button with a 'Not trusted' label", async () => {
renderComponent({ isUserVerified: true });
await act(flushPromises);
await flushPromises();
const button = screen.getByRole("button", { name: device.displayName });
expect(button).toHaveTextContent(`${device.displayName}Not trusted`);
@ -785,7 +788,7 @@ describe("<DeviceItem />", () => {
it("with verified device only, displays no button without a label", async () => {
setMockDeviceTrust(true);
renderComponent();
await act(flushPromises);
await flushPromises();
expect(screen.getByText(device.displayName!)).toBeInTheDocument();
expect(screen.queryByText(/trusted/)).not.toBeInTheDocument();
@ -798,7 +801,7 @@ describe("<DeviceItem />", () => {
mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
mockClient.getUserId.mockReturnValueOnce(defaultUserId);
renderComponent();
await act(flushPromises);
await flushPromises();
// set trust to be false for isVerified, true for isCrossSigningVerified
deferred.resolve({
@ -814,7 +817,7 @@ describe("<DeviceItem />", () => {
it("with verified user and device, displays no button and a 'Trusted' label", async () => {
setMockDeviceTrust(true);
renderComponent({ isUserVerified: true });
await act(flushPromises);
await flushPromises();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
expect(screen.getByText(device.displayName!)).toBeInTheDocument();
@ -824,7 +827,7 @@ describe("<DeviceItem />", () => {
it("does not call verifyDevice if client.getUser returns null", async () => {
mockClient.getUser.mockReturnValueOnce(null);
renderComponent();
await act(flushPromises);
await flushPromises();
const button = screen.getByRole("button", { name: device.displayName! });
expect(button).toBeInTheDocument();
@ -839,7 +842,7 @@ describe("<DeviceItem />", () => {
// even more mocking
mockClient.isGuest.mockReturnValueOnce(true);
renderComponent();
await act(flushPromises);
await flushPromises();
const button = screen.getByRole("button", { name: device.displayName! });
expect(button).toBeInTheDocument();
@ -851,7 +854,7 @@ describe("<DeviceItem />", () => {
it("with display name", async () => {
const { container } = renderComponent();
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@ -859,7 +862,7 @@ describe("<DeviceItem />", () => {
it("without display name", async () => {
const device = { deviceId: "deviceId" } as Device;
const { container } = renderComponent({ device, userId: defaultUserId });
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@ -867,7 +870,7 @@ describe("<DeviceItem />", () => {
it("ambiguous display name", async () => {
const device = { deviceId: "deviceId", ambiguous: true, displayName: "my display name" };
const { container } = renderComponent({ device, userId: defaultUserId });
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@ -1033,9 +1036,7 @@ describe("<UserOptionsSection />", () => {
expect(inviteSpy).toHaveBeenCalledWith([member.userId]);
// check that the test error message is displayed
await waitFor(() => {
expect(screen.getByText(mockErrorMessage.message)).toBeInTheDocument();
});
await expect(screen.findByText(mockErrorMessage.message)).resolves.toBeInTheDocument();
});
it("if calling .invite throws something strange, show default error message", async () => {
@ -1048,9 +1049,7 @@ describe("<UserOptionsSection />", () => {
await userEvent.click(inviteButton);
// check that the default test error message is displayed
await waitFor(() => {
expect(screen.getByText(/operation failed/i)).toBeInTheDocument();
});
await expect(screen.findByText(/operation failed/i)).resolves.toBeInTheDocument();
});
it.each([

View file

@ -0,0 +1,90 @@
/*
* 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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
import { UrlPreviewSettings } from "../../../../../src/components/views/room_settings/UrlPreviewSettings.tsx";
import SettingsStore from "../../../../../src/settings/SettingsStore.ts";
import dis from "../../../../../src/dispatcher/dispatcher.ts";
import { Action } from "../../../../../src/dispatcher/actions.ts";
describe("UrlPreviewSettings", () => {
let client: MatrixClient;
let room: Room;
beforeEach(() => {
client = createTestClient();
room = mkStubRoom("roomId", "room", client);
});
afterEach(() => {
jest.restoreAllMocks();
});
function renderComponent() {
return render(<UrlPreviewSettings room={room} />, withClientContextRenderOptions(client));
}
it("should display the correct preview when the setting is in a loading state", () => {
jest.spyOn(client, "getCrypto").mockReturnValue(undefined);
const { asFragment } = renderComponent();
expect(screen.getByText("URL Previews")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
const { asFragment } = renderComponent();
await waitFor(() => {
expect(
screen.getByText(
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
it("should display the correct preview when the room is unencrypted and the url preview is enabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
jest.spyOn(dis, "fire").mockReturnValue(undefined);
const { asFragment } = renderComponent();
await waitFor(() => {
expect(screen.getByRole("button", { name: "enabled" })).toBeInTheDocument();
expect(
screen.getByText("URL previews are enabled by default for participants in this room."),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
screen.getByRole("button", { name: "enabled" }).click();
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
});
it("should display the correct preview when the room is unencrypted and the url preview is disabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
const { asFragment } = renderComponent();
await waitFor(() => {
expect(screen.getByRole("button", { name: "disabled" })).toBeInTheDocument();
expect(
screen.getByText("URL previews are disabled by default for participants in this room."),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,236 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UrlPreviewSettings should display the correct preview when the room is encrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
</p>
<p>
In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.
</p>
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
/>
</label>
<div
aria-checked="true"
aria-disabled="false"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is disabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
</p>
<p>
<span>
You have
</span>
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
disabled
</div>
URL previews by default.
<p />
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div>
URL previews are disabled by default for participants in this room.
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable inline URL previews by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
</p>
<p>
<span>
You have
</span>
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
enabled
</div>
URL previews by default.
<p />
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div>
URL previews are enabled by default for participants in this room.
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable inline URL previews by default
</span>
</label>
<div
aria-checked="true"
aria-disabled="false"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the setting is in a loading state 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_content"
>
<svg
class="_icon_1ye7b_27"
fill="currentColor"
height="1em"
style="width: 20px; height: 20px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2Z"
fill-rule="evenodd"
/>
</svg>
</div>
</fieldset>
</DocumentFragment>
`;

View file

@ -260,7 +260,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@ -285,7 +285,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@ -301,6 +301,8 @@ describe("EventTile", () => {
[EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"],
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity has changed"],
])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
mxEvent = await mkEncryptedMatrixEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
@ -314,7 +316,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
expect(e2eIcons).toHaveLength(1);
@ -346,7 +348,7 @@ describe("EventTile", () => {
await mxEvent.attemptDecryption(mockCrypto);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@ -400,7 +402,7 @@ describe("EventTile", () => {
const roomContext = getRoomContext(room, {});
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@ -451,7 +453,7 @@ describe("EventTile", () => {
const roomContext = getRoomContext(room, {});
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);

View file

@ -22,7 +22,17 @@ exports[`EventTileThreadToolbar renders 1`] = `
role="button"
tabindex="-1"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
</div>
</div>
</DocumentFragment>

View file

@ -8,7 +8,16 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render, RenderResult, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
import {
act,
fireEvent,
render,
RenderResult,
screen,
waitFor,
waitForElementToBeRemoved,
cleanup,
} from "jest-matrix-react";
import { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked, MockedObject } from "jest-mock";
@ -361,6 +370,7 @@ describe("MemberList", () => {
afterEach(() => {
jest.restoreAllMocks();
cleanup();
});
const renderComponent = () => {
@ -397,21 +407,22 @@ describe("MemberList", () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(room, "canInvite").mockReturnValue(false);
renderComponent();
await flushPromises();
const { findByLabelText } = renderComponent();
// button rendered but disabled
expect(screen.getByText("Invite to this room")).toHaveAttribute("aria-disabled", "true");
await expect(findByLabelText("You do not have permission to invite users")).resolves.toHaveAttribute(
"aria-disabled",
"true",
);
});
it("renders enabled invite button when current user is a member and has rights to invite", async () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(room, "canInvite").mockReturnValue(true);
renderComponent();
await flushPromises();
const { findByText } = renderComponent();
expect(screen.getByText("Invite to this room")).not.toBeDisabled();
await expect(findByText("Invite to this room")).resolves.not.toBeDisabled();
});
it("opens room inviter on button click", async () => {

View file

@ -42,17 +42,13 @@ import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/t
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
const openStickerPicker = async (): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Sticker"));
});
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Sticker"));
};
const startVoiceMessage = async (): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Voice Message"));
});
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Voice Message"));
};
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
@ -61,7 +57,7 @@ const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState
MatrixClientPeg.safeGet(),
state,
);
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
act(() => SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording));
};
const expectVoiceMessageRecordingTriggered = (): void => {
@ -97,6 +93,45 @@ describe("MessageComposer", () => {
});
});
it("wysiwyg correctly persists state to and from localStorage", async () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
const messageText = "Test Text";
await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const { renderResult, rawComponent } = wrapAndRender({ room });
const { unmount } = renderResult;
await flushPromises();
const key = `mx_wysiwyg_state_${room.roomId}`;
await userEvent.click(screen.getByRole("textbox"));
fireEvent.input(screen.getByRole("textbox"), {
data: messageText,
inputType: "insertText",
});
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
// Wait for event dispatch to happen
await flushPromises();
// assert there is state persisted
expect(localStorage.getItem(key)).toBeNull();
// ensure the right state was persisted to localStorage
unmount();
// assert the persisted state
expect(JSON.parse(localStorage.getItem(key)!)).toStrictEqual({
content: messageText,
isRichText: true,
});
// ensure the correct state is re-loaded
render(rawComponent);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
}, 10000);
describe("for a Room", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
@ -185,14 +220,12 @@ describe("MessageComposer", () => {
[true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => {
beforeEach(async () => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
await act(() => SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value));
wrapAndRender({ room });
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
});
await userEvent.click(screen.getByLabelText("More options"));
});
it(`should${value || "not"} display the button`, () => {
it(`should${value ? "" : " not"} display the button`, () => {
if (value) {
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
@ -205,15 +238,17 @@ describe("MessageComposer", () => {
describe(`and setting ${setting} to ${!value}`, () => {
beforeEach(async () => {
// simulate settings update
await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value);
dis.dispatch(
{
action: Action.SettingUpdated,
settingName: setting,
newValue: !value,
},
true,
);
await act(async () => {
await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value);
dis.dispatch(
{
action: Action.SettingUpdated,
settingName: setting,
newValue: !value,
},
true,
);
});
});
it(`should${!value || "not"} display the button`, () => {
@ -273,7 +308,7 @@ describe("MessageComposer", () => {
beforeEach(async () => {
wrapAndRender({ room }, true, true);
await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {});
act(() => resizeCallback(UI_EVENTS.Resize, {}));
});
it("should close the menu", () => {
@ -295,7 +330,7 @@ describe("MessageComposer", () => {
beforeEach(async () => {
wrapAndRender({ room }, true, false);
await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {});
act(() => resizeCallback(UI_EVENTS.Resize, {}));
});
it("should close the menu", () => {
@ -443,51 +478,6 @@ describe("MessageComposer", () => {
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
});
});
it("wysiwyg correctly persists state to and from localStorage", async () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
const messageText = "Test Text";
await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const { renderResult, rawComponent } = wrapAndRender({ room });
const { unmount, rerender } = renderResult;
await act(async () => {
await flushPromises();
});
const key = `mx_wysiwyg_state_${room.roomId}`;
await act(async () => {
await userEvent.click(screen.getByRole("textbox"));
});
fireEvent.input(screen.getByRole("textbox"), {
data: messageText,
inputType: "insertText",
});
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// assert there is state persisted
expect(localStorage.getItem(key)).toBeNull();
// ensure the right state was persisted to localStorage
unmount();
// assert the persisted state
expect(JSON.parse(localStorage.getItem(key)!)).toStrictEqual({
content: messageText,
isRichText: true,
});
// ensure the correct state is re-loaded
rerender(rawComponent);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
}, 10000);
});
function wrapAndRender(

View file

@ -8,9 +8,19 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { EventType, JoinRule, MatrixEvent, PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import {
EventType,
JoinRule,
MatrixEvent,
PendingEventOrdering,
Room,
RoomStateEvent,
RoomMember,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import {
act,
createEvent,
fireEvent,
getAllByLabelText,
@ -632,6 +642,52 @@ describe("RoomHeader", () => {
expect(asFragment()).toMatchSnapshot();
});
it("updates the icon when the encryption status changes", async () => {
// The room starts verified
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
render(<RoomHeader room={room} />, getWrapper());
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
// A new member joins, and the room becomes unverified
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
act(() => {
room.emit(
RoomStateEvent.Members,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "join",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
room.currentState,
new RoomMember(room.roomId, "@alice:example.org"),
);
});
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
// The user becomes verified
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
act(() => {
MatrixClientPeg.get()!.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(true, true, true, false),
);
});
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
// An unverified device is added
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
act(() => {
MatrixClientPeg.get()!.emit(CryptoEvent.DevicesUpdated, ["@alice:example.org"], false);
});
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
});
});
it("renders additionalButtons", async () => {

View file

@ -27,7 +27,6 @@ import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import DocumentOffset from "../../../../../src/editor/offset";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { IRoomState, MainSplitContentType } from "../../../../../src/components/structures/RoomView";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { mockPlatformPeg } from "../../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import { addTextToComposer } from "../../../../test-utils/composer";
@ -78,16 +77,15 @@ describe("<SendMessageComposer/>", () => {
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
};
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 +99,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 +115,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 +132,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 +147,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 +362,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}>
@ -389,7 +386,7 @@ describe("<SendMessageComposer/>", () => {
it("correctly persists state to and from localStorage", () => {
const props = { replyToEvent: mockEvent };
const { container, unmount, rerender } = getComponent(props);
let { container, unmount } = getComponent(props);
addTextToComposer(container, "Test Text");
@ -406,7 +403,7 @@ describe("<SendMessageComposer/>", () => {
});
// ensure the correct model is re-loaded
rerender(getRawComponent(props));
({ container, unmount } = getComponent(props));
expect(container.textContent).toBe("Test Text");
expect(spyDispatcher).toHaveBeenCalledWith({
action: "reply_to_event",
@ -417,7 +414,7 @@ describe("<SendMessageComposer/>", () => {
// now try with localStorage wiped out
unmount();
localStorage.removeItem(key);
rerender(getRawComponent(props));
({ container } = getComponent(props));
expect(container.textContent).toBe("");
});
@ -482,6 +479,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

@ -0,0 +1,534 @@
/*
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 { sleep } from "matrix-js-sdk/src/utils";
import {
EventType,
MatrixClient,
MatrixEvent,
Room,
RoomState,
RoomStateEvent,
RoomMember,
} from "matrix-js-sdk/src/matrix";
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { act, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { stubClient } from "../../../../test-utils";
import { UserIdentityWarning } from "../../../../../src/components/views/rooms/UserIdentityWarning";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
const ROOM_ID = "!room:id";
function mockRoom(): Room {
const room = {
getEncryptionTargetMembers: jest.fn(async () => []),
getMember: jest.fn((userId) => {}),
roomId: ROOM_ID,
shouldEncryptForInvitedMembers: jest.fn(() => true),
} as unknown as Room;
return room;
}
function mockRoomMember(userId: string, name?: string): RoomMember {
return {
userId,
name: name ?? userId,
rawDisplayName: name ?? userId,
roomId: ROOM_ID,
getMxcAvatarUrl: jest.fn(),
} as unknown as RoomMember;
}
function dummyRoomState(): RoomState {
return new RoomState(ROOM_ID);
}
/**
* Get the warning element, given the warning text (excluding the "Learn more"
* link). This is needed because the warning text contains a `<b>` tag, so the
* normal `getByText` doesn't work.
*/
function getWarningByText(text: string): Element {
return screen.getByText((content?: string, element?: Element | null): boolean => {
return (
!!element &&
element.classList.contains("mx_UserIdentityWarning_main") &&
element.textContent === text + " Learn more"
);
});
}
function renderComponent(client: MatrixClient, room: Room) {
return render(<UserIdentityWarning room={room} key={ROOM_ID} />, {
wrapper: ({ ...rest }) => <MatrixClientContext.Provider value={client} {...rest} />,
});
}
describe("UserIdentityWarning", () => {
let client: MatrixClient;
let room: Room;
beforeEach(async () => {
client = stubClient();
room = mockRoom();
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
});
afterEach(() => {
jest.restoreAllMocks();
});
// This tests the basic functionality of the component. If we have a room
// member whose identity needs accepting, we should display a warning. When
// the "OK" button gets pressed, it should call `pinCurrentUserIdentity`.
it("displays a warning when a user's identity needs approval", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
mockRoomMember("@alice:example.org", "Alice"),
]);
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
crypto.pinCurrentUserIdentity = jest.fn();
renderComponent(client, room);
await waitFor(() =>
expect(
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
).toBeInTheDocument(),
);
await userEvent.click(screen.getByRole("button")!);
await waitFor(() => expect(crypto.pinCurrentUserIdentity).toHaveBeenCalledWith("@alice:example.org"));
});
// We don't display warnings in non-encrypted rooms, but if encryption is
// enabled, then we should display a warning if there are any users whose
// identity need accepting.
it("displays pending warnings when encryption is enabled", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
mockRoomMember("@alice:example.org", "Alice"),
]);
// Start the room off unencrypted. We shouldn't display anything.
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
// Encryption gets enabled in the room. We should now warn that Alice's
// identity changed.
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(true);
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomEncryption,
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
await waitFor(() =>
expect(
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
).toBeInTheDocument(),
);
});
// When a user's identity needs approval, or has been approved, the display
// should update appropriately.
it("updates the display when identity changes", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
mockRoomMember("@alice:example.org", "Alice"),
]);
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, false),
);
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
// The user changes their identity, so we should show the warning.
act(() => {
client.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(false, false, false, true),
);
});
await waitFor(() =>
expect(
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
).toBeInTheDocument(),
);
// Simulate the user's new identity having been approved, so we no
// longer show the warning.
act(() => {
client.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(false, false, false, false),
);
});
await waitFor(() =>
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(),
);
});
// We only display warnings about users in the room. When someone
// joins/leaves, we should update the warning appropriately.
describe("updates the display when a member joins/leaves", () => {
it("when invited users can see encrypted messages", async () => {
// Nobody in the room yet
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(true);
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
// Alice joins. Her identity needs approval, so we should show a warning.
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "join",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
await waitFor(() =>
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
);
// Bob is invited. His identity needs approval, so we should show a
// warning for him after Alice's warning is resolved by her leaving.
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@bob:example.org",
content: {
membership: "invite",
},
room_id: ROOM_ID,
sender: "@carol:example.org",
}),
dummyRoomState(),
null,
);
// Alice leaves, so we no longer show her warning, but we will show
// a warning for Bob.
act(() => {
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "leave",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
});
await waitFor(() =>
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
);
await waitFor(() =>
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
);
});
it("when invited users cannot see encrypted messages", async () => {
// Nobody in the room yet
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
// Alice joins. Her identity needs approval, so we should show a warning.
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "join",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
await waitFor(() =>
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
);
// Bob is invited. His identity needs approval, but we don't encrypt
// to him, so we won't show a warning. (When Alice leaves, the
// display won't be updated to show a warningfor Bob.)
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@bob:example.org",
content: {
membership: "invite",
},
room_id: ROOM_ID,
sender: "@carol:example.org",
}),
dummyRoomState(),
null,
);
// Alice leaves, so we no longer show her warning, and we don't show
// a warning for Bob.
act(() => {
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "leave",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
});
await waitFor(() =>
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
);
await waitFor(() =>
expect(() => getWarningByText("@bob:example.org's identity appears to have changed.")).toThrow(),
);
});
it("when member leaves immediately after component is loaded", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockImplementation(async () => {
setTimeout(() => {
// Alice immediately leaves after we get the room
// membership, so we shouldn't show the warning any more
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "leave",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
});
return [mockRoomMember("@alice:example.org")];
});
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
renderComponent(client, room);
await sleep(10);
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
});
it("when member leaves immediately after joining", async () => {
// Nobody in the room yet
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
// Alice joins. Her identity needs approval, so we should show a warning.
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "join",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
// ... but she immediately leaves, so we shouldn't show the warning any more
client.emit(
RoomStateEvent.Events,
new MatrixEvent({
event_id: "$event_id",
type: EventType.RoomMember,
state_key: "@alice:example.org",
content: {
membership: "leave",
},
room_id: ROOM_ID,
sender: "@alice:example.org",
}),
dummyRoomState(),
null,
);
await sleep(10); // give it some time to finish
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
});
});
// When we have multiple users whose identity needs approval, one user's
// identity no longer needs approval (e.g. their identity was approved),
// then we show the next one.
it("displays the next user when the current user's identity is approved", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
mockRoomMember("@alice:example.org", "Alice"),
mockRoomMember("@bob:example.org"),
]);
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false, true),
);
renderComponent(client, room);
// We should warn about Alice's identity first.
await waitFor(() =>
expect(
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
).toBeInTheDocument(),
);
// Simulate Alice's new identity having been approved, so now we warn
// about Bob's identity.
act(() => {
client.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(false, false, false, false),
);
});
await waitFor(() =>
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
);
});
// If we get an update for a user's verification status while we're fetching
// that user's verification status, we should display based on the updated
// value.
describe("handles races between fetching verification status and receiving updates", () => {
// First case: check that if the update says that the user identity
// needs approval, but the fetch says it doesn't, we show the warning.
it("update says identity needs approval", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
mockRoomMember("@alice:example.org", "Alice"),
]);
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => {
act(() => {
client.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(false, false, false, true),
);
});
return Promise.resolve(new UserVerificationStatus(false, false, false, false));
});
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
await waitFor(() =>
expect(
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
).toBeInTheDocument(),
);
});
// Second case: check that if the update says that the user identity
// doesn't needs approval, but the fetch says it does, we don't show the
// warning.
it("update says identity doesn't need approval", async () => {
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
mockRoomMember("@alice:example.org", "Alice"),
]);
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
const crypto = client.getCrypto()!;
jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => {
act(() => {
client.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(false, false, false, false),
);
});
return Promise.resolve(new UserVerificationStatus(false, false, false, true));
});
renderComponent(client, room);
await sleep(10); // give it some time to finish initialising
await waitFor(() =>
expect(() =>
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
).toThrow(),
);
});
});
});

View file

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

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import "@testing-library/jest-dom";
import React from "react";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
@ -253,9 +253,7 @@ describe("EditWysiwygComposer", () => {
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
await flushPromises();
// Then we don't get it because we are disabled
expect(screen.getByRole("textbox")).not.toHaveFocus();

View file

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

View file

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

View file

@ -6,8 +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 { render, screen, waitFor } from "jest-matrix-react";
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor, cleanup } from "jest-matrix-react";
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);
@ -47,54 +48,13 @@ describe("AddRemoveThreepids", () => {
afterEach(() => {
jest.restoreAllMocks();
clearAllModals();
cleanup();
});
const clientProviderWrapper: React.FC = ({ children }: React.PropsWithChildren) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
);
it("should render a loader while loading", async () => {
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={true}
onChange={() => {}}
/>,
);
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
});
it("should render email addresses", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[EMAIL1]}
isLoading={false}
onChange={() => {}}
/>,
);
expect(container).toMatchSnapshot();
});
it("should render phone numbers", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[PHONE1]}
isLoading={false}
onChange={() => {}}
/>,
);
expect(container).toMatchSnapshot();
});
it("should handle no email addresses", async () => {
const { container } = render(
<AddRemoveThreepids
@ -106,6 +66,7 @@ describe("AddRemoveThreepids", () => {
/>,
);
await expect(screen.findByText("Email Address")).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
@ -126,7 +87,7 @@ describe("AddRemoveThreepids", () => {
},
);
const input = screen.getByRole("textbox", { name: "Email Address" });
const input = await screen.findByRole("textbox", { name: "Email Address" });
await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
@ -165,7 +126,7 @@ describe("AddRemoveThreepids", () => {
},
);
const input = screen.getByRole("textbox", { name: "Email Address" });
const input = await screen.findByRole("textbox", { name: "Email Address" });
await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
@ -209,7 +170,7 @@ describe("AddRemoveThreepids", () => {
},
);
const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ });
const countryDropdown = await screen.findByRole("button", { name: /Country Dropdown/ });
await userEvent.click(countryDropdown);
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
await userEvent.click(gbOption);
@ -222,13 +183,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",
@ -269,7 +230,7 @@ describe("AddRemoveThreepids", () => {
},
);
const removeButton = screen.getByRole("button", { name: /Remove/ });
const removeButton = await screen.findByRole("button", { name: /Remove/ });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
@ -296,7 +257,7 @@ describe("AddRemoveThreepids", () => {
},
);
const removeButton = screen.getByRole("button", { name: /Remove/ });
const removeButton = await screen.findByRole("button", { name: /Remove/ });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
@ -325,7 +286,7 @@ describe("AddRemoveThreepids", () => {
},
);
const removeButton = screen.getByRole("button", { name: /Remove/ });
const removeButton = await screen.findByRole("button", { name: /Remove/ });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${PHONE1.address}?`)).toBeVisible();
@ -356,7 +317,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(EMAIL1.address)).toBeVisible();
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
const shareButton = screen.getByRole("button", { name: /Share/ });
await userEvent.click(shareButton);
@ -407,7 +368,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(PHONE1.address)).toBeVisible();
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
const shareButton = screen.getByRole("button", { name: /Share/ });
await userEvent.click(shareButton);
@ -451,7 +412,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(EMAIL1.address)).toBeVisible();
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
await userEvent.click(revokeButton);
@ -474,11 +435,169 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(PHONE1.address)).toBeVisible();
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
await userEvent.click(revokeButton);
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),
}),
);
});
it("should render a loader while loading", async () => {
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={true}
onChange={() => {}}
/>,
);
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
});
it("should render email addresses", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[EMAIL1]}
isLoading={false}
onChange={() => {}}
/>,
);
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
it("should render phone numbers", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[PHONE1]}
isLoading={false}
onChange={() => {}}
/>,
);
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
});

View file

@ -6,7 +6,7 @@ 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 } from "jest-matrix-react";
import { render, screen, fireEvent } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import AvatarSetting from "../../../../../src/components/views/settings/AvatarSetting";
@ -16,6 +16,9 @@ const BASE64_GIF = "R0lGODlhAQABAAAAACw=";
const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", {
type: "image/gif",
});
const GENERIC_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "not-avatar.doc", {
type: "application/msword",
});
describe("<AvatarSetting />", () => {
beforeEach(() => {
@ -70,4 +73,45 @@ describe("<AvatarSetting />", () => {
expect(onChange).toHaveBeenCalledWith(AVATAR_FILE);
});
it("should noop when selecting no file", async () => {
const onChange = jest.fn();
render(
<AvatarSetting
placeholderId="blee"
placeholderName="boo"
avatar="mxc://example.org/my-avatar"
avatarAltText="Avatar of Peter Fox"
onChange={onChange}
/>,
);
const fileInput = screen.getByAltText("Upload");
// Can't use userEvent.upload here as it doesn't support uploading invalid files
fireEvent.change(fileInput, { target: { files: [] } });
expect(onChange).not.toHaveBeenCalled();
});
it("should show error if user tries to use non-image file", async () => {
const onChange = jest.fn();
render(
<AvatarSetting
placeholderId="blee"
placeholderName="boo"
avatar="mxc://example.org/my-avatar"
avatarAltText="Avatar of Peter Fox"
onChange={onChange}
/>,
);
const fileInput = screen.getByAltText("Upload");
// Can't use userEvent.upload here as it doesn't support uploading invalid files
fireEvent.change(fileInput, { target: { files: [GENERIC_FILE] } });
expect(onChange).not.toHaveBeenCalled();
await expect(screen.findByRole("heading", { name: "Upload Failed" })).resolves.toBeInTheDocument();
});
});

View file

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

View file

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

View file

@ -28,14 +28,13 @@ describe("<SecureBackupPanel />", () => {
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(),
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
getClientWellKnown: jest.fn(),
});
const getComponent = () => render(<SecureBackupPanel />);
beforeEach(() => {
client.getKeyBackupVersion.mockResolvedValue({
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
version: "1",
algorithm: "test",
auth_data: {
@ -52,7 +51,6 @@ describe("<SecureBackupPanel />", () => {
});
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
client.getKeyBackupVersion.mockClear();
mocked(accessSecretStorage).mockClear().mockResolvedValue();
});
@ -65,8 +63,8 @@ describe("<SecureBackupPanel />", () => {
});
it("handles error fetching backup", async () => {
// getKeyBackupVersion can fail for various reasons
client.getKeyBackupVersion.mockImplementation(async () => {
// getKeyBackupInfo can fail for various reasons
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
throw new Error("beep beep");
});
const renderResult = getComponent();
@ -75,9 +73,9 @@ describe("<SecureBackupPanel />", () => {
});
it("handles absence of backup", async () => {
client.getKeyBackupVersion.mockResolvedValue(null);
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
getComponent();
// flush getKeyBackupVersion promise
// flush getKeyBackupInfo promise
await flushPromises();
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
});
@ -120,7 +118,7 @@ describe("<SecureBackupPanel />", () => {
});
it("deletes backup after confirmation", async () => {
client.getKeyBackupVersion
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo")
.mockResolvedValueOnce({
version: "1",
algorithm: "test",
@ -157,7 +155,7 @@ describe("<SecureBackupPanel />", () => {
// flush checkKeyBackup promise
await flushPromises();
client.getKeyBackupVersion.mockClear();
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear();
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
fireEvent.click(screen.getByText("Reset"));
@ -167,7 +165,7 @@ describe("<SecureBackupPanel />", () => {
await flushPromises();
// backup status refreshed
expect(client.getKeyBackupVersion).toHaveBeenCalled();
expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled();
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
});
});

View file

@ -11,14 +11,14 @@ exports[`AddRemoveThreepids should handle no email addresses 1`] = `
>
<input
autocomplete="email"
id="mx_Field_3"
id="mx_Field_1"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_3"
for="mx_Field_1"
>
Email Address
</label>
@ -61,14 +61,14 @@ exports[`AddRemoveThreepids should render email addresses 1`] = `
>
<input
autocomplete="email"
id="mx_Field_1"
id="mx_Field_14"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_1"
for="mx_Field_14"
>
Email Address
</label>
@ -148,14 +148,14 @@ exports[`AddRemoveThreepids should render phone numbers 1`] = `
</span>
<input
autocomplete="tel-national"
id="mx_Field_2"
id="mx_Field_15"
label="Phone Number"
placeholder="Phone Number"
type="text"
value=""
/>
<label
for="mx_Field_2"
for="mx_Field_15"
>
Phone Number
</label>

View file

@ -79,9 +79,7 @@ describe("<LoginWithQR />", () => {
describe("MSC4108", () => {
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
<React.StrictMode>
<LoginWithQR {...defaultProps} {...props} />
</React.StrictMode>
<LoginWithQR {...defaultProps} {...props} />
);
test("render QR then back", async () => {

View file

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

View file

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

View file

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

View file

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

View file

@ -27,16 +27,19 @@ describe("RolesRoomSettingsTab", () => {
let cli: MatrixClient;
let room: Room;
const renderTab = (propRoom: Room = room): RenderResult => {
return render(<RolesRoomSettingsTab room={propRoom} />, withClientContextRenderOptions(cli));
const renderTab = async (propRoom: Room = room): Promise<RenderResult> => {
const renderResult = render(<RolesRoomSettingsTab room={propRoom} />, withClientContextRenderOptions(cli));
// Wait for the tab to be ready
await waitFor(() => expect(screen.getByText("Permissions")).toBeInTheDocument());
return renderResult;
};
const getVoiceBroadcastsSelect = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts']")!;
const getVoiceBroadcastsSelect = async (): Promise<Element> => {
return (await renderTab()).container.querySelector("select[label='Voice broadcasts']")!;
};
const getVoiceBroadcastsSelectedOption = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked")!;
const getVoiceBroadcastsSelectedOption = async (): Promise<Element> => {
return (await renderTab()).container.querySelector("select[label='Voice broadcasts'] option:checked")!;
};
beforeEach(() => {
@ -45,7 +48,7 @@ describe("RolesRoomSettingsTab", () => {
room = mkStubRoom(roomId, "test room", cli);
});
it("should allow an Admin to demote themselves but not others", () => {
it("should allow an Admin to demote themselves but not others", async () => {
mocked(cli.getRoom).mockReturnValue(room);
// @ts-ignore - mocked doesn't support overloads properly
mocked(room.currentState.getStateEvents).mockImplementation((type, key) => {
@ -67,19 +70,19 @@ describe("RolesRoomSettingsTab", () => {
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
const { container } = await renderTab();
expect(container.querySelector(`[placeholder="${cli.getUserId()}"]`)).not.toBeDisabled();
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
});
it("should initially show »Moderator« permission for »Voice broadcasts«", () => {
expect(getVoiceBroadcastsSelectedOption().textContent).toBe("Moderator");
it("should initially show »Moderator« permission for »Voice broadcasts«", async () => {
expect((await getVoiceBroadcastsSelectedOption()).textContent).toBe("Moderator");
});
describe("when setting »Default« permission for »Voice broadcasts«", () => {
beforeEach(() => {
fireEvent.change(getVoiceBroadcastsSelect(), {
beforeEach(async () => {
fireEvent.change(await getVoiceBroadcastsSelect(), {
target: { value: 0 },
});
});
@ -122,12 +125,12 @@ describe("RolesRoomSettingsTab", () => {
});
describe("Join Element calls", () => {
it("defaults to moderator for joining calls", () => {
expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
it("defaults to moderator for joining calls", async () => {
expect(getJoinCallSelectedOption(await renderTab())?.textContent).toBe("Moderator");
});
it("can change joining calls power level", () => {
const tab = renderTab();
it("can change joining calls power level", async () => {
const tab = await renderTab();
fireEvent.change(getJoinCallSelect(tab), {
target: { value: 0 },
@ -143,12 +146,12 @@ describe("RolesRoomSettingsTab", () => {
});
describe("Start Element calls", () => {
it("defaults to moderator for starting calls", () => {
expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
it("defaults to moderator for starting calls", async () => {
expect(getStartCallSelectedOption(await renderTab())?.textContent).toBe("Moderator");
});
it("can change starting calls power level", () => {
const tab = renderTab();
it("can change starting calls power level", async () => {
const tab = await renderTab();
fireEvent.change(getStartCallSelect(tab), {
target: { value: 0 },
@ -164,10 +167,10 @@ describe("RolesRoomSettingsTab", () => {
});
});
it("hides when group calls disabled", () => {
it("hides when group calls disabled", async () => {
setGroupCallsEnabled(false);
const tab = renderTab();
const tab = await renderTab();
expect(getStartCallSelect(tab)).toBeFalsy();
expect(getStartCallSelectedOption(tab)).toBeFalsy();
@ -250,7 +253,7 @@ describe("RolesRoomSettingsTab", () => {
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
const { container } = await renderTab();
const selector = container.querySelector(`[placeholder="${cli.getUserId()}"]`)!;
fireEvent.change(selector, { target: { value: "50" } });

View file

@ -75,7 +75,7 @@ describe("<SecurityRoomSettingsTab />", () => {
beforeEach(async () => {
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
client.isRoomEncrypted.mockReturnValue(false);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
client.getClientWellKnown.mockReturnValue(undefined);
jest.spyOn(SettingsStore, "getValue").mockRestore();
@ -313,7 +313,7 @@ describe("<SecurityRoomSettingsTab />", () => {
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
fireEvent.click(screen.getByLabelText("Encrypted"));
@ -330,7 +330,7 @@ describe("<SecurityRoomSettingsTab />", () => {
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
fireEvent.click(screen.getByLabelText("Encrypted"));
@ -416,12 +416,12 @@ describe("<SecurityRoomSettingsTab />", () => {
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
});
it("displays unencrypted rooms with toggle disabled", () => {
it("displays unencrypted rooms with toggle disabled", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();

View file

@ -34,7 +34,6 @@ describe("<SecurityUserSettingsTab />", () => {
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getKeyBackupVersion: jest.fn(),
});
const sdkContext = new SdkContextClass();

View file

@ -277,9 +277,7 @@ describe("<SessionManagerTab />", () => {
mockClient.getDevices.mockRejectedValue({ httpStatus: 404 });
const { container } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy();
});
@ -302,9 +300,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledTimes(3);
expect(
@ -337,9 +333,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
// twice for each device
expect(mockClient.getAccountData).toHaveBeenCalledTimes(4);
@ -356,9 +350,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId, queryByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesDevice.device_id);
// application metadata section not rendered
@ -369,9 +361,7 @@ describe("<SessionManagerTab />", () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] });
const { queryByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(queryByTestId("other-sessions-section")).toBeFalsy();
});
@ -382,9 +372,7 @@ describe("<SessionManagerTab />", () => {
});
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(getByTestId("other-sessions-section")).toBeTruthy();
});
@ -395,9 +383,7 @@ describe("<SessionManagerTab />", () => {
});
const { getByTestId, container } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
fireEvent.click(getByTestId("unverified-devices-cta"));
@ -908,7 +894,8 @@ describe("<SessionManagerTab />", () => {
});
it("deletes a device when interactive auth is not required", async () => {
mockClient.deleteMultipleDevices.mockResolvedValue({});
const deferredDeleteMultipleDevices = defer<{}>();
mockClient.deleteMultipleDevices.mockReturnValue(deferredDeleteMultipleDevices.promise);
mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
});
@ -933,6 +920,7 @@ describe("<SessionManagerTab />", () => {
fireEvent.click(signOutButton);
await confirmSignout(getByTestId);
await prom;
deferredDeleteMultipleDevices.resolve({});
// delete called
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
@ -991,7 +979,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId, getByLabelText } = render(getComponent());
await act(flushPromises);
await flushPromises();
// reset mock count after initial load
mockClient.getDevices.mockClear();
@ -1025,7 +1013,7 @@ describe("<SessionManagerTab />", () => {
fireEvent.submit(getByLabelText("Password"));
});
await act(flushPromises);
await flushPromises();
// called again with auth
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([alicesMobileDevice.device_id], {
@ -1551,7 +1539,7 @@ describe("<SessionManagerTab />", () => {
});
const { getByTestId, container } = render(getComponent());
await act(flushPromises);
await flushPromises();
// filter for inactive sessions
await setFilter(container, DeviceSecurityVariation.Inactive);
@ -1577,9 +1565,7 @@ describe("<SessionManagerTab />", () => {
it("lets you change the pusher state", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
@ -1598,9 +1584,7 @@ describe("<SessionManagerTab />", () => {
it("lets you change the local notification settings state", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesDevice.device_id);
@ -1621,9 +1605,7 @@ describe("<SessionManagerTab />", () => {
it("updates the UI when another session changes the local notifications", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesDevice.device_id);

View file

@ -42,14 +42,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
>
<input
autocomplete="email"
id="mx_Field_9"
id="mx_Field_3"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_9"
for="mx_Field_3"
>
Email Address
</label>
@ -145,14 +145,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
</span>
<input
autocomplete="tel-national"
id="mx_Field_10"
id="mx_Field_4"
label="Phone Number"
placeholder="Phone Number"
type="text"
value=""
/>
<label
for="mx_Field_10"
for="mx_Field_4"
>
Phone Number
</label>

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>
@ -370,7 +388,7 @@ exports[`<SessionManagerTab /> goes to filtered list from security recommendatio
>
<input
aria-label="Select all"
aria-labelledby=":r4e:"
aria-labelledby=":r3s:"
data-testid="device-select-all-checkbox"
id="device-select-all-checkbox"
type="checkbox"

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
@ -125,7 +135,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.897 2.817 2.336 4.733 5.223.76a1 1 0 0 1 .555 1.705L17.23 13.7l.892 5.202a1 1 0 0 1-1.45 1.054L12 17.5l-4.672 2.456a1 1 0 0 1-1.451-1.054l.892-5.202-3.78-3.685a1 1 0 0 1 .555-1.706l5.223-.759 2.336-4.733a1 1 0 0 1 1.794 0Z"
/>
</svg>
Favourites
</div>
<div
@ -157,7 +177,20 @@ 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 15c-1.1 0-2.042-.392-2.825-1.175C8.392 13.042 8 12.1 8 11s.392-2.042 1.175-2.825C9.958 7.392 10.9 7 12 7s2.042.392 2.825 1.175C15.608 8.958 16 9.9 16 11s-.392 2.042-1.175 2.825C14.042 14.608 13.1 15 12 15Z"
/>
<path
d="M19.528 18.583A9.962 9.962 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 2.52.933 4.824 2.472 6.583A9.976 9.976 0 0 0 12 22a9.976 9.976 0 0 0 7.528-3.417ZM8.75 16.388c-.915.221-1.818.538-2.709.95a8 8 0 1 1 11.918 0 14.679 14.679 0 0 0-2.709-.95A13.76 13.76 0 0 0 12 16c-1.1 0-2.183.13-3.25.387Z"
/>
</svg>
People
</div>
<div
@ -312,7 +345,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
@ -376,7 +419,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.897 2.817 2.336 4.733 5.223.76a1 1 0 0 1 .555 1.705L17.23 13.7l.892 5.202a1 1 0 0 1-1.45 1.054L12 17.5l-4.672 2.456a1 1 0 0 1-1.451-1.054l.892-5.202-3.78-3.685a1 1 0 0 1 .555-1.706l5.223-.759 2.336-4.733a1 1 0 0 1 1.794 0Z"
/>
</svg>
Favourites
</div>
<div
@ -408,7 +461,20 @@ 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 15c-1.1 0-2.042-.392-2.825-1.175C8.392 13.042 8 12.1 8 11s.392-2.042 1.175-2.825C9.958 7.392 10.9 7 12 7s2.042.392 2.825 1.175C15.608 8.958 16 9.9 16 11s-.392 2.042-1.175 2.825C14.042 14.608 13.1 15 12 15Z"
/>
<path
d="M19.528 18.583A9.962 9.962 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 2.52.933 4.824 2.472 6.583A9.976 9.976 0 0 0 12 22a9.976 9.976 0 0 0 7.528-3.417ZM8.75 16.388c-.915.221-1.818.538-2.709.95a8 8 0 1 1 11.918 0 14.679 14.679 0 0 0-2.709-.95A13.76 13.76 0 0 0 12 16c-1.1 0-2.183.13-3.25.387Z"
/>
</svg>
People
</div>
<div

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { ComponentProps } from "react";
import { mocked, Mocked } from "jest-mock";
import { act, render, RenderResult } from "jest-matrix-react";
import { render, RenderResult } from "jest-matrix-react";
import { TypedEventEmitter, IMyDevice, MatrixClient, Device } from "matrix-js-sdk/src/matrix";
import { VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api";
@ -63,9 +63,7 @@ describe("VerificationRequestToast", () => {
otherDeviceId,
});
const result = renderComponent({ request });
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(result.container).toMatchSnapshot();
});
@ -76,9 +74,7 @@ describe("VerificationRequestToast", () => {
otherUserId,
});
const result = renderComponent({ request });
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(result.container).toMatchSnapshot();
});
@ -89,9 +85,7 @@ describe("VerificationRequestToast", () => {
otherUserId,
});
renderComponent({ request, toastKey: "testKey" });
await act(async () => {
await flushPromises();
});
await flushPromises();
const dismiss = jest.spyOn(ToastStore.sharedInstance(), "dismissToast");
Object.defineProperty(request, "accepting", { value: true });

View file

@ -332,7 +332,7 @@ describe("linkify-matrix", () => {
const event = new MouseEvent("mousedown");
event.preventDefault = jest.fn();
handlers.click(event);
handlers!.click(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({
@ -372,7 +372,7 @@ describe("linkify-matrix", () => {
const event = new MouseEvent("mousedown");
event.preventDefault = jest.fn();
handlers.click(event);
handlers!.click(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({

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

@ -189,8 +189,7 @@ describe("MemberListStore", () => {
});
it("does not use lazy loading on encrypted rooms", async () => {
client.isRoomEncrypted = jest.fn();
mocked(client.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
const { joined } = await store.loadMemberList(roomId);
expect(joined).toEqual([room.getMember(alice)]);

View file

@ -338,7 +338,7 @@ describe("RoomViewStore", function () {
});
dis.dispatch({ action: Action.ViewRoom, room_id: roomId });
await untilDispatch(Action.ActiveRoomChanged, dis);
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(roomId, "com.famedly.marked_unread", {
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(roomId, "m.marked_unread", {
unread: false,
});
});

View file

@ -37,6 +37,7 @@ describe("SetupEncryptionStore", () => {
getDeviceVerificationStatus: jest.fn(),
isDehydrationSupported: jest.fn().mockResolvedValue(false),
startDehydration: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<CryptoApi>;
client.getCrypto.mockReturnValue(mockCrypto);
@ -170,15 +171,10 @@ describe("SetupEncryptionStore", () => {
await setupEncryptionStore.resetConfirm();
expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), true);
expect(makeRequest).toHaveBeenCalledWith({
identifier: {
type: "m.id.user",
user: "@userId:matrix.org",
},
password: cachedPassword,
type: "m.login.password",
user: "@userId:matrix.org",
expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), {
accountPassword: cachedPassword,
forceReset: true,
resetCrossSigning: true,
});
});
});

View file

@ -91,7 +91,7 @@ describe("RoomNotificationState", () => {
const listener = jest.fn();
roomNotifState.addListener(NotificationStateEvents.Update, listener);
const accountDataEvent = {
getType: () => "com.famedly.marked_unread",
getType: () => "m.marked_unread",
getContent: () => {
return { unread: true };
},

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

@ -65,7 +65,8 @@ describe("UnverifiedSessionToast", () => {
});
};
it("should render as expected", () => {
it("should render as expected", async () => {
await expect(screen.findByText("New login. Was this you?")).resolves.toBeInTheDocument();
expect(renderResult.baseElement).toMatchSnapshot();
});

View file

@ -23,6 +23,8 @@ import {
concat,
asyncEvery,
asyncSome,
asyncSomeParallel,
asyncFilter,
} from "../../../src/utils/arrays";
type TestParams = { input: number[]; output: number[] };
@ -460,4 +462,34 @@ describe("arrays", () => {
expect(predicate).toHaveBeenCalledWith(2);
});
});
describe("asyncSomeParallel", () => {
it("when called with an empty array, it should return false", async () => {
expect(await asyncSomeParallel([], jest.fn().mockResolvedValue(true))).toBe(false);
});
it("when all the predicates return false", async () => {
expect(await asyncSomeParallel([1, 2, 3], jest.fn().mockResolvedValue(false))).toBe(false);
});
it("when all the predicates return true", async () => {
expect(await asyncSomeParallel([1, 2, 3], jest.fn().mockResolvedValue(true))).toBe(true);
});
it("when one of the predicate return true", async () => {
const predicate = jest.fn().mockImplementation((value) => Promise.resolve(value === 2));
expect(await asyncSomeParallel([1, 2, 3], predicate)).toBe(true);
});
});
describe("asyncFilter", () => {
it("when called with an empty array, it should return an empty array", async () => {
expect(await asyncFilter([], jest.fn().mockResolvedValue(true))).toEqual([]);
});
it("should filter the content", async () => {
const predicate = jest.fn().mockImplementation((value) => Promise.resolve(value === 2));
expect(await asyncFilter([1, 2, 3], predicate)).toEqual([2]);
});
});
});

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

Some files were not shown because too many files have changed in this diff Show more