Migrate to React 18 createRoot API (#28256)

* Migrate to React 18 createRoot API

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Discard changes to src/components/views/settings/devices/DeviceDetails.tsx

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Attempt to stabilise test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* legacyRoot?

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28499/head
Michael Telatynski 2024-11-20 13:29:23 +00:00 committed by GitHub
parent 48fd330dd9
commit ca33d9165a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 719 additions and 731 deletions

View File

@ -75,6 +75,7 @@ interface State {
}
export default class ForgotPassword extends React.Component<Props, State> {
private unmounted = false;
private reset: PasswordReset;
private fieldPassword: Field | null = null;
private fieldPasswordConfirm: Field | null = null;
@ -108,14 +109,20 @@ export default class ForgotPassword extends React.Component<Props, State> {
}
}
public componentWillUnmount(): void {
this.unmounted = true;
}
private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<void> {
try {
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl);
if (this.unmounted) return;
this.setState({
serverIsAlive: true,
});
} catch (e: any) {
if (this.unmounted) return;
const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError(
e,
"forgot_password",

View File

@ -8,7 +8,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 * as ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import React, { StrictMode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
@ -93,7 +93,9 @@ export async function loadApp(fragParams: {}): Promise<void> {
function setWindowMatrixChat(matrixChat: MatrixChat): void {
window.matrixChat = matrixChat;
}
ReactDOM.render(await module.loadApp(fragParams, setWindowMatrixChat), document.getElementById("matrixchat"));
const app = await module.loadApp(fragParams, setWindowMatrixChat);
const root = createRoot(document.getElementById("matrixchat")!);
root.render(app);
}
export async function showError(title: string, messages?: string[]): Promise<void> {
@ -101,11 +103,11 @@ export async function showError(title: string, messages?: string[]): Promise<voi
/* webpackChunkName: "error-view" */
"../async-components/structures/ErrorView"
);
ReactDOM.render(
const root = createRoot(document.getElementById("matrixchat")!);
root.render(
<StrictMode>
<ErrorView title={title} messages={messages} />
</StrictMode>,
document.getElementById("matrixchat"),
);
}
@ -114,11 +116,11 @@ export async function showIncompatibleBrowser(onAccept: () => void): Promise<voi
/* webpackChunkName: "error-view" */
"../async-components/structures/ErrorView"
);
ReactDOM.render(
const root = createRoot(document.getElementById("matrixchat")!);
root.render(
<StrictMode>
<UnsupportedBrowserView onAccept={onAccept} />
</StrictMode>,
document.getElementById("matrixchat"),
);
}

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

@ -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

@ -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 {
@ -79,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
@ -162,7 +162,7 @@ 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]);
});
@ -390,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]");
@ -423,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

@ -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";
@ -163,7 +163,7 @@ describe("<MatrixChat />", () => {
let initPromise: Promise<void> | undefined;
let defaultProps: ComponentProps<typeof MatrixChat>;
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) =>
render(<MatrixChat {...defaultProps} {...props} />);
render(<MatrixChat {...defaultProps} {...props} />, { legacyRoot: true });
// make test results readable
filterConsole(
@ -201,7 +201,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 +263,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 +328,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 +956,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();
@ -1126,9 +1128,11 @@ describe("<MatrixChat />", () => {
await getComponentAndLogin();
bootstrapDeferred.resolve();
act(() => 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();
});
});
});
@ -1397,7 +1401,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 () => {

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

@ -23,14 +23,22 @@ import {
} from "matrix-js-sdk/src/matrix";
import { CryptoApi, UserVerificationStatus } 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 {
stubClient,
mockPlatformPeg,
unmockPlatformPeg,
wrapInMatrixClientContext,
flushPromises,
mkEvent,
setupAsyncStoreWithClient,
@ -45,7 +53,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { Action } from "../../../../src/dispatcher/actions";
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";
@ -64,8 +72,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>;
@ -106,9 +113,10 @@ describe("RoomView", () => {
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 = () => {
@ -120,26 +128,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;
@ -167,22 +179,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!;
};
@ -193,7 +207,7 @@ describe("RoomView", () => {
});
describe("when there is an old room", () => {
let instance: _RoomView;
let instance: RoomView;
let oldRoom: Room;
beforeEach(async () => {
@ -217,11 +231,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 () => {
@ -252,15 +266,17 @@ describe("RoomView", () => {
cli.isRoomEncrypted.mockReturnValue(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,
}),
]);
await act(() =>
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]),
);
// URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false);
@ -270,7 +286,7 @@ describe("RoomView", () => {
const roomViewInstance = await getRoomViewInstance();
const oldTimeline = roomViewInstance.state.liveTimeline;
room.getUnfilteredTimelineSet().resetLiveTimeline();
act(() => room.getUnfilteredTimelineSet().resetLiveTimeline());
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
});
@ -287,7 +303,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);
@ -429,6 +445,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";
@ -514,184 +718,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(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);
// @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, 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 });
});
});

View File

@ -215,34 +215,33 @@ describe("ThreadPanel", () => {
myThreads!.addLiveEvent(mixedThread.rootEvent);
myThreads!.addLiveEvent(ownThread.rootEvent);
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);
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 } from "jest-matrix-react";
import {
ReceiptType,
EventTimelineSet,
@ -205,8 +205,10 @@ describe("TimelinePanel", () => {
manageReadReceipts={true}
ref={ref}
/>,
{ legacyRoot: true },
);
await flushPromises();
await waitFor(() => expect(ref.current).toBeTruthy());
timelinePanel = ref.current!;
};
@ -255,14 +257,16 @@ describe("TimelinePanel", () => {
describe("and reading the timeline", () => {
beforeEach(async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
await flushPromises();
await act(async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
await flushPromises();
// @ts-ignore
await timelinePanel.sendReadReceipts();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker();
// @ts-ignore
await timelinePanel.sendReadReceipts();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker();
});
});
it("should send a fully read marker and a public receipt", async () => {
@ -276,7 +280,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", () => {
@ -315,7 +319,7 @@ describe("TimelinePanel", () => {
it("should send a fully read marker and a private receipt", async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
act(() => timelineSet.addLiveEvent(ev1, {}));
await flushPromises();
// @ts-ignore
@ -326,6 +330,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
@ -334,7 +339,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()));
});
});
});
@ -361,11 +366,11 @@ describe("TimelinePanel", () => {
it("should send receipts but no fully_read when reading the thread timeline", async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(threadEv1, {});
act(() => timelineSet.addLiveEvent(threadEv1, {}));
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();
@ -1021,7 +1026,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,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 } 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> => {
@ -80,18 +70,11 @@ describe("<ForgotPassword>", () => {
await clearAllModals();
});
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
describe("when starting a password reset flow", () => {
beforeEach(() => {
renderResult = render(
<ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />,
{ legacyRoot: true },
);
});
@ -128,8 +111,10 @@ 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();
});
});
@ -142,8 +127,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 +141,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 +162,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 +199,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 +228,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 +268,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 +281,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 +347,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 +370,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

@ -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

@ -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", () => {
@ -125,6 +125,7 @@ describe("CreateSecretStorageDialog", () => {
resetFunctionCallLog.push("resetKeyBackup");
});
await flushPromises();
result.getByRole("button", { name: "Continue" }).click();
await result.findByText("Your keys are now being backed up from this device.");

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

@ -10,7 +10,7 @@ import React from "react";
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 {
ApprovalOpts,
@ -29,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";
@ -59,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();
@ -160,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}>
@ -233,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") {
@ -263,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
@ -279,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,7 +335,7 @@ describe("AppTile", () => {
let renderResult: RenderResult;
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} />
@ -353,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
});
@ -459,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

@ -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>

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

@ -48,6 +48,7 @@ describe("DateSeparator", () => {
<MatrixClientContext.Provider value={mockClient}>
<DateSeparator {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
{ legacyRoot: true },
);
type TestCase = [string, number, string];
@ -264,10 +265,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();
});
[
@ -280,19 +283,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

@ -27,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", () => {
@ -120,9 +120,9 @@ describe("EncryptionEvent", () => {
renderEncryptionEvent(client, event);
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId);
checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
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

@ -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, waitForElementToBeRemoved } from "jest-matrix-react";
import { EventTimeline, MatrixEvent, Room, M_TEXT } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
@ -127,6 +127,7 @@ describe("<MPollEndBody />", () => {
expect(container).toMatchSnapshot();
await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument());
await waitForElementToBeRemoved(() => getByRole("progressbar"));
expect(mockClient.fetchRoomEvent).toHaveBeenCalledWith(roomId, pollStartEvent.getId());

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

@ -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 } 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";
@ -199,6 +199,7 @@ describe("<UserInfo />", () => {
return render(<UserInfo {...defaultProps} {...props} />, {
wrapper: Wrapper,
legacyRoot: true,
});
};
@ -439,7 +440,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 +460,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 +491,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 +539,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 +584,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 +619,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,7 +654,7 @@ describe("<UserInfo />", () => {
room: mockRoom,
});
await waitFor(() => expect(screen.getByRole("button", { name: "Deactivate user" })).toBeInTheDocument());
await expect(screen.findByRole("button", { name: "Deactivate user" })).resolves.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
@ -666,7 +667,7 @@ describe("<UserInfo />", () => {
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 +678,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 +769,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 +777,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 +786,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 +799,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 +815,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 +825,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 +840,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 +852,7 @@ describe("<DeviceItem />", () => {
it("with display name", async () => {
const { container } = renderComponent();
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@ -859,7 +860,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 +868,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 +1034,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 +1047,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

@ -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);
@ -314,7 +314,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 +346,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 +400,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 +451,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

@ -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(
@ -529,7 +519,7 @@ function wrapAndRender(
);
return {
rawComponent: getRawComponent(props, roomContext, mockClient),
renderResult: render(getRawComponent(props, roomContext, mockClient)),
renderResult: render(getRawComponent(props, roomContext, mockClient), { legacyRoot: true }),
roomContext,
};
}

View File

@ -385,7 +385,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");
@ -402,7 +402,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",
@ -413,7 +413,7 @@ describe("<SendMessageComposer/>", () => {
// now try with localStorage wiped out
unmount();
localStorage.removeItem(key);
rerender(getRawComponent(props));
({ container } = getComponent(props));
expect(container.textContent).toBe("");
});

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

@ -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, screen, waitFor } from "jest-matrix-react";
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";
@ -48,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
@ -107,6 +66,7 @@ describe("AddRemoveThreepids", () => {
/>,
);
await expect(screen.findByText("Email Address")).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
@ -127,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);
@ -166,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);
@ -210,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);
@ -270,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();
@ -297,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();
@ -326,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();
@ -357,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);
@ -408,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);
@ -452,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);
@ -475,7 +435,7 @@ 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);
@ -596,4 +556,48 @@ describe("AddRemoveThreepids", () => {
}),
);
});
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

@ -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

@ -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

@ -388,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

@ -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

@ -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

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

View File

@ -103,6 +103,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
</p>
<div
class="mx_Flex mx_ErrorView_buttons"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
@ -152,6 +153,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
</h2>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
>
<a
class="_button_i91xf_17 _has-icon_i91xf_66"
@ -221,6 +223,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
</h2>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x);"
>
<a
href="https://apps.apple.com/app/vector/id1083446067"

View File

@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { showError, showIncompatibleBrowser } from "../../../src/vector/init.tsx";
import fetchMock from "fetch-mock-jest";
import { waitFor, screen } from "jest-matrix-react";
import { loadApp, showError, showIncompatibleBrowser } from "../../../src/vector/init.tsx";
import SdkConfig from "../../../src/SdkConfig.ts";
import MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
function setUpMatrixChatDiv() {
document.getElementById("matrixchat")?.remove();
@ -19,6 +24,7 @@ describe("showIncompatibleBrowser", () => {
it("should match snapshot", async () => {
await showIncompatibleBrowser(jest.fn());
await screen.findByText("Element does not support this browser");
expect(document.getElementById("matrixchat")).toMatchSnapshot();
});
});
@ -28,6 +34,19 @@ describe("showError", () => {
it("should match snapshot", async () => {
await showError("Error title", ["msg1", "msg2"]);
await screen.findByText("Error title");
expect(document.getElementById("matrixchat")).toMatchSnapshot();
});
});
describe("loadApp", () => {
beforeEach(setUpMatrixChatDiv);
it("should set window.matrixChat to the MatrixChat instance", async () => {
fetchMock.get("https://matrix.org/_matrix/client/versions", { versions: ["v1.6"] });
SdkConfig.put({ default_server_config: { "m.homeserver": { base_url: "https://matrix.org" } } });
await loadApp({});
await waitFor(() => expect(window.matrixChat).toBeInstanceOf(MatrixChat));
});
});

View File

@ -90,9 +90,7 @@ describe("VoiceBroadcastPreRecordingPip", () => {
beforeEach(async () => {
renderResult = render(<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={preRecording} />);
await act(async () => {
flushPromises();
});
await flushPromises();
});
it("should match the snapshot", () => {

View File

@ -8,7 +8,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 { ClientEvent, MatrixClient, MatrixEvent, SyncState } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
@ -61,9 +61,7 @@ describe("VoiceBroadcastRecordingPip", () => {
jest.spyOn(recording, "pause");
jest.spyOn(recording, "resume");
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
await act(async () => {
flushPromises();
});
await flushPromises();
};
const itShouldShowTheBroadcastRoom = () => {
@ -152,8 +150,9 @@ describe("VoiceBroadcastRecordingPip", () => {
describe("and clicking the stop button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("Stop Recording"));
await screen.findByText("Stop live broadcasting?");
// modal rendering has some weird sleeps
await sleep(100);
await sleep(200);
});
it("should display the confirm end dialog", () => {