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>
This commit is contained in:
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> { export default class ForgotPassword extends React.Component<Props, State> {
private unmounted = false;
private reset: PasswordReset; private reset: PasswordReset;
private fieldPassword: Field | null = null; private fieldPassword: Field | null = null;
private fieldPasswordConfirm: 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> { private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<void> {
try { try {
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl); await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl);
if (this.unmounted) return;
this.setState({ this.setState({
serverIsAlive: true, serverIsAlive: true,
}); });
} catch (e: any) { } catch (e: any) {
if (this.unmounted) return;
const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError( const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError(
e, e,
"forgot_password", "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. 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 React, { StrictMode } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -93,7 +93,9 @@ export async function loadApp(fragParams: {}): Promise<void> {
function setWindowMatrixChat(matrixChat: MatrixChat): void { function setWindowMatrixChat(matrixChat: MatrixChat): void {
window.matrixChat = matrixChat; 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> { 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" */ /* webpackChunkName: "error-view" */
"../async-components/structures/ErrorView" "../async-components/structures/ErrorView"
); );
ReactDOM.render( const root = createRoot(document.getElementById("matrixchat")!);
root.render(
<StrictMode> <StrictMode>
<ErrorView title={title} messages={messages} /> <ErrorView title={title} messages={messages} />
</StrictMode>, </StrictMode>,
document.getElementById("matrixchat"),
); );
} }
@ -114,11 +116,11 @@ export async function showIncompatibleBrowser(onAccept: () => void): Promise<voi
/* webpackChunkName: "error-view" */ /* webpackChunkName: "error-view" */
"../async-components/structures/ErrorView" "../async-components/structures/ErrorView"
); );
ReactDOM.render( const root = createRoot(document.getElementById("matrixchat")!);
root.render(
<StrictMode> <StrictMode>
<UnsupportedBrowserView onAccept={onAccept} /> <UnsupportedBrowserView onAccept={onAccept} />
</StrictMode>, </StrictMode>,
document.getElementById("matrixchat"),
); );
} }

View file

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

View file

@ -197,7 +197,7 @@ export const clearAllModals = async (): Promise<void> => {
// Prevent modals from leaking and polluting other tests // Prevent modals from leaking and polluting other tests
let keepClosingModals = true; let keepClosingModals = true;
while (keepClosingModals) { while (keepClosingModals) {
keepClosingModals = Modal.closeCurrentModal(); keepClosingModals = await act(() => Modal.closeCurrentModal());
// Then wait for the screen to update (probably React rerender and async/await). // 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 // 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 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 userEvent from "@testing-library/user-event";
import { import {
@ -79,15 +79,15 @@ describe("RovingTabIndex", () => {
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// focus on 2nd button and test it is the only active one // 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]); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
// focus on 1st button and test it is the only active one // 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]); checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
// check that the active button does not change even on an explicit blur event // 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]); checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
// update the children, it should remain on the same button // update the children, it should remain on the same button
@ -162,7 +162,7 @@ describe("RovingTabIndex", () => {
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// focus on 2nd button and test it is the only active one // 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]); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
}); });
@ -390,7 +390,7 @@ describe("RovingTabIndex", () => {
</RovingTabIndexProvider>, </RovingTabIndexProvider>,
); );
container.querySelectorAll("button")[0].focus(); act(() => container.querySelectorAll("button")[0].focus());
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
await userEvent.keyboard("[ArrowDown]"); await userEvent.keyboard("[ArrowDown]");
@ -423,7 +423,7 @@ describe("RovingTabIndex", () => {
</RovingTabIndexProvider>, </RovingTabIndexProvider>,
); );
container.querySelectorAll("button")[0].focus(); act(() => container.querySelectorAll("button")[0].focus());
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
const button = container.querySelectorAll("button")[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 "core-js/stable/structured-clone";
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import React, { ComponentProps } from "react"; 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 fetchMock from "fetch-mock-jest";
import { Mocked, mocked } from "jest-mock"; import { Mocked, mocked } from "jest-mock";
import { ClientEvent, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix"; import { ClientEvent, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
@ -163,7 +163,7 @@ describe("<MatrixChat />", () => {
let initPromise: Promise<void> | undefined; let initPromise: Promise<void> | undefined;
let defaultProps: ComponentProps<typeof MatrixChat>; let defaultProps: ComponentProps<typeof MatrixChat>;
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) => const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) =>
render(<MatrixChat {...defaultProps} {...props} />); render(<MatrixChat {...defaultProps} {...props} />, { legacyRoot: true });
// make test results readable // make test results readable
filterConsole( filterConsole(
@ -201,7 +201,7 @@ describe("<MatrixChat />", () => {
// we are logged in, but are still waiting for the /sync to complete // we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Syncing…"); await screen.findByText("Syncing…");
// initial sync // initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null); await act(() => client.emit(ClientEvent.Sync, SyncState.Prepared, null));
} }
// let things settle // 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 // 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) // (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(); localStorage.clear();
}); });
@ -328,7 +328,7 @@ describe("<MatrixChat />", () => {
expect(within(dialog).getByText(errorMessage)).toBeInTheDocument(); expect(within(dialog).getByText(errorMessage)).toBeInTheDocument();
// just check we're back on welcome page // 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(() => { beforeEach(() => {
@ -956,9 +956,11 @@ describe("<MatrixChat />", () => {
await screen.findByText("Powered by Matrix"); await screen.findByText("Powered by Matrix");
// go to login page // go to login page
defaultDispatcher.dispatch({ act(() =>
action: "start_login", defaultDispatcher.dispatch({
}); action: "start_login",
}),
);
await flushPromises(); await flushPromises();
@ -1126,9 +1128,11 @@ describe("<MatrixChat />", () => {
await getComponentAndLogin(); 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() { function simulateSessionLockClaim() {
localStorage.setItem("react_sdk_session_lock_claimant", "testtest"); 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 () => { it("after a session is restored", async () => {

View file

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

View file

@ -23,14 +23,22 @@ import {
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { KnownMembership } from "matrix-js-sdk/src/types"; 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 userEvent from "@testing-library/user-event";
import { import {
stubClient, stubClient,
mockPlatformPeg, mockPlatformPeg,
unmockPlatformPeg, unmockPlatformPeg,
wrapInMatrixClientContext,
flushPromises, flushPromises,
mkEvent, mkEvent,
setupAsyncStoreWithClient, setupAsyncStoreWithClient,
@ -45,7 +53,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { Action } from "../../../../src/dispatcher/actions"; import { Action } from "../../../../src/dispatcher/actions";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload"; 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 ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../../src/settings/SettingLevel";
@ -64,8 +72,7 @@ import WidgetStore from "../../../../src/stores/WidgetStore";
import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRoomErrorPayload"; import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRoomErrorPayload";
import { SearchScope } from "../../../../src/Searching"; import { SearchScope } from "../../../../src/Searching";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
const RoomView = wrapInMatrixClientContext(_RoomView);
describe("RoomView", () => { describe("RoomView", () => {
let cli: MockedObject<MatrixClient>; let cli: MockedObject<MatrixClient>;
@ -106,9 +113,10 @@ describe("RoomView", () => {
afterEach(() => { afterEach(() => {
unmockPlatformPeg(); unmockPlatformPeg();
jest.clearAllMocks(); 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) { if (stores.roomViewStore.getRoomId() !== room.roomId) {
const switchedRoom = new Promise<void>((resolve) => { const switchedRoom = new Promise<void>((resolve) => {
const subFn = () => { const subFn = () => {
@ -120,26 +128,30 @@ describe("RoomView", () => {
stores.roomViewStore.on(UPDATE_EVENT, subFn); stores.roomViewStore.on(UPDATE_EVENT, subFn);
}); });
defaultDispatcher.dispatch<ViewRoomPayload>({ act(() =>
action: Action.ViewRoom, defaultDispatcher.dispatch<ViewRoomPayload>({
room_id: room.roomId, action: Action.ViewRoom,
metricsTrigger: undefined, room_id: room.roomId,
}); metricsTrigger: undefined,
}),
);
await switchedRoom; await switchedRoom;
} }
const roomView = render( const roomView = render(
<SDKContext.Provider value={stores}> <MatrixClientContext.Provider value={cli}>
<RoomView <SDKContext.Provider value={stores}>
// threepidInvite should be optional on RoomView props <RoomView
// it is treated as optional in RoomView // threepidInvite should be optional on RoomView props
threepidInvite={undefined as any} // it is treated as optional in RoomView
resizeNotifier={new ResizeNotifier()} threepidInvite={undefined as any}
forceTimeline={false} resizeNotifier={new ResizeNotifier()}
wrappedRef={ref as any} forceTimeline={false}
/> ref={ref}
</SDKContext.Provider>, />
</SDKContext.Provider>
</MatrixClientContext.Provider>,
); );
await flushPromises(); await flushPromises();
return roomView; return roomView;
@ -167,22 +179,24 @@ describe("RoomView", () => {
} }
const roomView = render( const roomView = render(
<SDKContext.Provider value={stores}> <MatrixClientContext.Provider value={cli}>
<RoomView <SDKContext.Provider value={stores}>
// threepidInvite should be optional on RoomView props <RoomView
// it is treated as optional in RoomView // threepidInvite should be optional on RoomView props
threepidInvite={undefined as any} // it is treated as optional in RoomView
resizeNotifier={new ResizeNotifier()} threepidInvite={undefined as any}
forceTimeline={false} resizeNotifier={new ResizeNotifier()}
onRegistered={jest.fn()} forceTimeline={false}
/> onRegistered={jest.fn()}
</SDKContext.Provider>, />
</SDKContext.Provider>
</MatrixClientContext.Provider>,
); );
await flushPromises(); await flushPromises();
return roomView; return roomView;
}; };
const getRoomViewInstance = async (): Promise<_RoomView> => { const getRoomViewInstance = async (): Promise<RoomView> => {
const ref = createRef<_RoomView>(); const ref = createRef<RoomView>();
await mountRoomView(ref); await mountRoomView(ref);
return ref.current!; return ref.current!;
}; };
@ -193,7 +207,7 @@ describe("RoomView", () => {
}); });
describe("when there is an old room", () => { describe("when there is an old room", () => {
let instance: _RoomView; let instance: RoomView;
let oldRoom: Room; let oldRoom: Room;
beforeEach(async () => { beforeEach(async () => {
@ -217,11 +231,11 @@ describe("RoomView", () => {
describe("and feature_dynamic_room_predecessors is enabled", () => { describe("and feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => { beforeEach(() => {
instance.setState({ msc3946ProcessDynamicPredecessor: true }); act(() => instance.setState({ msc3946ProcessDynamicPredecessor: true }));
}); });
afterEach(() => { afterEach(() => {
instance.setState({ msc3946ProcessDynamicPredecessor: false }); act(() => instance.setState({ msc3946ProcessDynamicPredecessor: false }));
}); });
it("should pass the setting to findPredecessor", async () => { it("should pass the setting to findPredecessor", async () => {
@ -252,15 +266,17 @@ describe("RoomView", () => {
cli.isRoomEncrypted.mockReturnValue(true); cli.isRoomEncrypted.mockReturnValue(true);
// and fake an encryption event into the room to prompt it to re-check // and fake an encryption event into the room to prompt it to re-check
room.addLiveEvents([ await act(() =>
new MatrixEvent({ room.addLiveEvents([
type: "m.room.encryption", new MatrixEvent({
sender: cli.getUserId()!, type: "m.room.encryption",
content: {}, sender: cli.getUserId()!,
event_id: "someid", content: {},
room_id: room.roomId, event_id: "someid",
}), room_id: room.roomId,
]); }),
]),
);
// URL previews should now be disabled // URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false); expect(roomViewInstance.state.showUrlPreview).toBe(false);
@ -270,7 +286,7 @@ describe("RoomView", () => {
const roomViewInstance = await getRoomViewInstance(); const roomViewInstance = await getRoomViewInstance();
const oldTimeline = roomViewInstance.state.liveTimeline; const oldTimeline = roomViewInstance.state.liveTimeline;
room.getUnfilteredTimelineSet().resetLiveTimeline(); act(() => room.getUnfilteredTimelineSet().resetLiveTimeline());
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline); expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
}); });
@ -287,7 +303,7 @@ describe("RoomView", () => {
await renderRoomView(); await renderRoomView();
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId); expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
cli.emit(ClientEvent.Room, room); act(() => cli.emit(ClientEvent.Room, room));
// called again after room event // called again after room event
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2); 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", () => { describe("when there is a RoomView", () => {
const widget1Id = "widget1"; const widget1Id = "widget1";
const widget2Id = "widget2"; 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(mixedThread.rootEvent);
myThreads!.addLiveEvent(ownThread.rootEvent); myThreads!.addLiveEvent(ownThread.rootEvent);
let events: EventData[] = [];
const renderResult = render(<TestThreadPanel />); const renderResult = render(<TestThreadPanel />);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => { await waitFor(() => {
events = findEvents(renderResult.container); const events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(3); 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()); await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
toggleThreadFilter(renderResult.container, ThreadFilterType.My); toggleThreadFilter(renderResult.container, ThreadFilterType.My);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => { await waitFor(() => {
events = findEvents(renderResult.container); const events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(2); 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); toggleThreadFilter(renderResult.container, ThreadFilterType.All);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => { await waitFor(() => {
events = findEvents(renderResult.container); const events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(3); 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 () => { it("correctly filters Thread List with a single, unparticipated thread", async () => {
@ -261,28 +260,27 @@ describe("ThreadPanel", () => {
const [allThreads] = room.threadsTimelineSets; const [allThreads] = room.threadsTimelineSets;
allThreads!.addLiveEvent(otherThread.rootEvent); allThreads!.addLiveEvent(otherThread.rootEvent);
let events: EventData[] = [];
const renderResult = render(<TestThreadPanel />); const renderResult = render(<TestThreadPanel />);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => { await waitFor(() => {
events = findEvents(renderResult.container); const events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(1); 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()); await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
toggleThreadFilter(renderResult.container, ThreadFilterType.My); toggleThreadFilter(renderResult.container, ThreadFilterType.My);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => { await waitFor(() => {
events = findEvents(renderResult.container); const events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(0); expect(events).toHaveLength(0);
}); });
toggleThreadFilter(renderResult.container, ThreadFilterType.All); toggleThreadFilter(renderResult.container, ThreadFilterType.All);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => { await waitFor(() => {
events = findEvents(renderResult.container); const events = findEvents(renderResult.container);
expect(findEvents(renderResult.container)).toHaveLength(1); 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. 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 { import {
ReceiptType, ReceiptType,
EventTimelineSet, EventTimelineSet,
@ -205,8 +205,10 @@ describe("TimelinePanel", () => {
manageReadReceipts={true} manageReadReceipts={true}
ref={ref} ref={ref}
/>, />,
{ legacyRoot: true },
); );
await flushPromises(); await flushPromises();
await waitFor(() => expect(ref.current).toBeTruthy());
timelinePanel = ref.current!; timelinePanel = ref.current!;
}; };
@ -255,14 +257,16 @@ describe("TimelinePanel", () => {
describe("and reading the timeline", () => { describe("and reading the timeline", () => {
beforeEach(async () => { beforeEach(async () => {
await renderTimelinePanel(); await act(async () => {
timelineSet.addLiveEvent(ev1, {}); await renderTimelinePanel();
await flushPromises(); timelineSet.addLiveEvent(ev1, {});
await flushPromises();
// @ts-ignore // @ts-ignore
await timelinePanel.sendReadReceipts(); await timelinePanel.sendReadReceipts();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel. // @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker(); await timelinePanel.updateReadMarker();
});
}); });
it("should send a fully read marker and a public receipt", async () => { it("should send a fully read marker and a public receipt", async () => {
@ -276,7 +280,7 @@ describe("TimelinePanel", () => {
client.setRoomReadMarkers.mockClear(); client.setRoomReadMarkers.mockClear();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel. // @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker(); await act(() => timelinePanel.updateReadMarker());
}); });
it("should not send receipts again", () => { it("should not send receipts again", () => {
@ -315,7 +319,7 @@ describe("TimelinePanel", () => {
it("should send a fully read marker and a private receipt", async () => { it("should send a fully read marker and a private receipt", async () => {
await renderTimelinePanel(); await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {}); act(() => timelineSet.addLiveEvent(ev1, {}));
await flushPromises(); await flushPromises();
// @ts-ignore // @ts-ignore
@ -326,6 +330,7 @@ describe("TimelinePanel", () => {
// Expect the fully_read marker not to be send yet // Expect the fully_read marker not to be send yet
expect(client.setRoomReadMarkers).not.toHaveBeenCalled(); expect(client.setRoomReadMarkers).not.toHaveBeenCalled();
await flushPromises();
client.sendReadReceipt.mockClear(); client.sendReadReceipt.mockClear();
// @ts-ignore simulate user activity // @ts-ignore simulate user activity
@ -334,7 +339,7 @@ describe("TimelinePanel", () => {
// It should not send the receipt again. // It should not send the receipt again.
expect(client.sendReadReceipt).not.toHaveBeenCalledWith(ev1, ReceiptType.ReadPrivate); expect(client.sendReadReceipt).not.toHaveBeenCalledWith(ev1, ReceiptType.ReadPrivate);
// Expect the fully_read marker to be sent after user activity. // 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 () => { it("should send receipts but no fully_read when reading the thread timeline", async () => {
await renderTimelinePanel(); await renderTimelinePanel();
timelineSet.addLiveEvent(threadEv1, {}); act(() => timelineSet.addLiveEvent(threadEv1, {}));
await flushPromises(); await flushPromises();
// @ts-ignore // @ts-ignore
await timelinePanel.sendReadReceipts(); await act(() => timelinePanel.sendReadReceipts());
// fully_read is not supported for threads per spec // fully_read is not supported for threads per spec
expect(client.setRoomReadMarkers).not.toHaveBeenCalled(); expect(client.setRoomReadMarkers).not.toHaveBeenCalled();
@ -1021,7 +1026,7 @@ describe("TimelinePanel", () => {
await waitFor(() => expectEvents(container, [events[1]])); await waitFor(() => expectEvents(container, [events[1]]));
}); });
defaultDispatcher.fire(Action.DumpDebugLogs); act(() => defaultDispatcher.fire(Action.DumpDebugLogs));
await waitFor(() => await waitFor(() =>
expect(spy).toHaveBeenCalledWith(expect.stringContaining("TimelinePanel(Room): Debugging info for roomId")), 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 React from "react";
import { mocked } from "jest-mock"; 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 userEvent from "@testing-library/user-event";
import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
import ForgotPassword from "../../../../../src/components/structures/auth/ForgotPassword"; import ForgotPassword from "../../../../../src/components/structures/auth/ForgotPassword";
import { ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig";
import { import { clearAllModals, filterConsole, stubClient, waitEnoughCyclesForModal } from "../../../../test-utils";
clearAllModals,
filterConsole,
flushPromisesWithFakeTimers,
stubClient,
waitEnoughCyclesForModal,
} from "../../../../test-utils";
import AutoDiscoveryUtils from "../../../../../src/utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../../../../src/utils/AutoDiscoveryUtils";
jest.mock("matrix-js-sdk/src/matrix", () => ({ jest.mock("matrix-js-sdk/src/matrix", () => ({
@ -39,11 +33,7 @@ describe("<ForgotPassword>", () => {
let renderResult: RenderResult; let renderResult: RenderResult;
const typeIntoField = async (label: string, value: string): Promise<void> => { const typeIntoField = async (label: string, value: string): Promise<void> => {
await act(async () => { await userEvent.type(screen.getByLabelText(label), value, { delay: null });
await userEvent.type(screen.getByLabelText(label), value, { delay: null });
// the message is shown after some time
jest.advanceTimersByTime(500);
});
}; };
const click = async (element: Element): Promise<void> => { const click = async (element: Element): Promise<void> => {
@ -80,18 +70,11 @@ describe("<ForgotPassword>", () => {
await clearAllModals(); await clearAllModals();
}); });
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
describe("when starting a password reset flow", () => { describe("when starting a password reset flow", () => {
beforeEach(() => { beforeEach(() => {
renderResult = render( renderResult = render(
<ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />, <ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />,
{ legacyRoot: true },
); );
}); });
@ -128,8 +111,10 @@ describe("<ForgotPassword>", () => {
await typeIntoField("Email address", "not en email"); await typeIntoField("Email address", "not en email");
}); });
it("should show a message about the wrong format", () => { it("should show a message about the wrong format", async () => {
expect(screen.getByText("The email address doesn't appear to be valid.")).toBeInTheDocument(); 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")); await click(screen.getByText("Send email"));
}); });
it("should show an email not found message", () => { it("should show an email not found message", async () => {
expect(screen.getByText("This email address was not found")).toBeInTheDocument(); await expect(screen.findByText("This email address was not found")).resolves.toBeInTheDocument();
}); });
}); });
@ -156,13 +141,12 @@ describe("<ForgotPassword>", () => {
await click(screen.getByText("Send email")); await click(screen.getByText("Send email"));
}); });
it("should show an info about that", () => { it("should show an info about that", async () => {
expect( await expect(
screen.getByText( screen.findByText(
"Cannot reach homeserver: " + "Cannot reach homeserver: Ensure you have a stable internet connection, or get in touch with the server admin",
"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")); await click(screen.getByText("Send email"));
}); });
it("should show the server error", () => { it("should show the server error", async () => {
expect(screen.queryByText("server down")).toBeInTheDocument(); await expect(screen.findByText("server down")).resolves.toBeInTheDocument();
}); });
}); });
@ -215,8 +199,6 @@ describe("<ForgotPassword>", () => {
describe("and clicking »Resend«", () => { describe("and clicking »Resend«", () => {
beforeEach(async () => { beforeEach(async () => {
await click(screen.getByText("Resend")); 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", () => { it("should should resend the mail and show the tooltip", () => {
@ -246,8 +228,10 @@ describe("<ForgotPassword>", () => {
await typeIntoField("Confirm new password", testPassword + "asd"); await typeIntoField("Confirm new password", testPassword + "asd");
}); });
it("should show an info about that", () => { it("should show an info about that", async () => {
expect(screen.getByText("New passwords must match each other.")).toBeInTheDocument(); await expect(
screen.findByText("New passwords must match each other."),
).resolves.toBeInTheDocument();
}); });
}); });
@ -284,7 +268,7 @@ describe("<ForgotPassword>", () => {
await click(screen.getByText("Reset password")); 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( expect(client.setPassword).toHaveBeenCalledWith(
{ {
type: "m.login.email.identity", type: "m.login.email.identity",
@ -297,19 +281,15 @@ describe("<ForgotPassword>", () => {
false, 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 // it should not retry to set the password
expect(client.setPassword).toHaveBeenCalledTimes(1); await waitFor(() => expect(client.setPassword).toHaveBeenCalledTimes(1));
}); });
}); });
describe("and submitting it", () => { describe("and submitting it", () => {
beforeEach(async () => { beforeEach(async () => {
await click(screen.getByText("Reset password")); await click(screen.getByText("Reset password"));
await waitEnoughCyclesForModal({ await waitEnoughCyclesForModal();
useFakeTimers: true,
});
}); });
it("should send the new password and show the click validation link dialog", async () => { 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(); expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
}); });
}); });
});
describe("and validating the link from the mail", () => { describe("and validating the link from the mail", () => {
beforeEach(async () => { beforeEach(async () => {
mocked(client.setPassword).mockResolvedValue({}); mocked(client.setPassword).mockResolvedValue({});
// be sure the next set password attempt was sent await click(screen.getByText("Reset password"));
jest.advanceTimersByTime(3000); // flush promises for the modal to disappear
// quad flush promises for the modal to disappear await waitEnoughCyclesForModal();
await flushPromisesWithFakeTimers(); await waitEnoughCyclesForModal();
await flushPromisesWithFakeTimers(); });
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
});
it("should display the confirm reset view and now show the dialog", () => { it("should display the confirm reset view and now show the dialog", async () => {
expect(screen.queryByText("Your password has been reset.")).toBeInTheDocument(); await expect(
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); 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 () => { beforeEach(async () => {
await click(screen.getByText("Sign out of all devices")); await click(screen.getByText("Sign out of all devices"));
await click(screen.getByText("Reset password")); await click(screen.getByText("Reset password"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
}); });
it("should show the sign out warning dialog", async () => { 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 () => { it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => {
render(<SpotlightDialog onFinished={() => null} />, { legacyRoot: false }); render(<SpotlightDialog onFinished={() => null} />);
jest.advanceTimersByTime(200); jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers(); await flushPromisesWithFakeTimers();
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(true); 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 { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils"; 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"; import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
describe("CreateSecretStorageDialog", () => { describe("CreateSecretStorageDialog", () => {
@ -125,6 +125,7 @@ describe("CreateSecretStorageDialog", () => {
resetFunctionCallLog.push("resetKeyBackup"); resetFunctionCallLog.push("resetKeyBackup");
}); });
await flushPromises();
result.getByRole("button", { name: "Continue" }).click(); result.getByRole("button", { name: "Continue" }).click();
await result.findByText("Your keys are now being backed up from this device."); 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 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 userEvent from "@testing-library/user-event";
import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix"; import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix";
@ -23,12 +23,12 @@ describe("ExportE2eKeysDialog", () => {
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it("should have disabled submit button initially", () => { it("should have disabled submit button initially", async () => {
const cli = createTestClient(); const cli = createTestClient();
const onFinished = jest.fn(); const onFinished = jest.fn();
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />); const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.click(container.querySelector("[type=submit]")!); await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
expect(screen.getByText("Enter passphrase")).toBeInTheDocument(); expect(screen.getByLabelText("Enter passphrase")).toBeInTheDocument();
}); });
it("should complain about weak passphrases", async () => { it("should complain about weak passphrases", async () => {
@ -38,7 +38,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />); const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
const input = screen.getByLabelText("Enter passphrase"); const input = screen.getByLabelText("Enter passphrase");
await userEvent.type(input, "password"); 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(); 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} />); const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), "ThisIsAMoreSecurePW123$$"); await userEvent.type(screen.getByLabelText("Enter passphrase"), "ThisIsAMoreSecurePW123$$");
await userEvent.type(screen.getByLabelText("Confirm passphrase"), "ThisIsAMoreSecurePW124$$"); 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(); await expect(screen.findByText("Passphrases must match")).resolves.toBeInTheDocument();
}); });
@ -74,7 +74,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={jest.fn()} />); const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={jest.fn()} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), passphrase); await userEvent.type(screen.getByLabelText("Enter passphrase"), passphrase);
await userEvent.type(screen.getByLabelText("Confirm 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 // Then it exports keys and encrypts them
await waitFor(() => expect(exportRoomKeysAsJson).toHaveBeenCalled()); 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 { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api"; import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk"; 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 userEvent from "@testing-library/user-event";
import { import {
ApprovalOpts, ApprovalOpts,
@ -29,7 +29,6 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext
import SettingsStore from "../../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../../src/settings/SettingsStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases"; import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore"; import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore";
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore"; import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore"; import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
import AppTile from "../../../../../src/components/views/elements/AppTile"; import AppTile from "../../../../../src/components/views/elements/AppTile";
@ -59,16 +58,6 @@ describe("AppTile", () => {
let app1: IApp; let app1: IApp;
let app2: 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 () => { beforeAll(async () => {
stubClient(); stubClient();
cli = MatrixClientPeg.safeGet(); cli = MatrixClientPeg.safeGet();
@ -160,29 +149,28 @@ describe("AppTile", () => {
/> />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
// Wait for RPS room 1 updates to fire act(() =>
const rpsUpdated = waitForRps("r1"); dis.dispatch({
dis.dispatch({ action: Action.ViewRoom,
action: Action.ViewRoom, room_id: "r1",
room_id: "r1", }),
}); );
await rpsUpdated;
expect(renderResult.getByText("Example 1")).toBeInTheDocument(); await expect(renderResult.findByText("Example 1")).resolves.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true); expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
const { container, asFragment } = renderResult; const { asFragment } = renderResult;
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
// We want to verify that as we change to room 2, we should close the // We want to verify that as we change to room 2, we should close the
// right panel and destroy the widget. // right panel and destroy the widget.
// Switch to room 2 // Switch to room 2
dis.dispatch({ act(() =>
action: Action.ViewRoom, dis.dispatch({
room_id: "r2", action: Action.ViewRoom,
}); room_id: "r2",
}),
);
renderResult.rerender( renderResult.rerender(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
@ -233,16 +221,17 @@ describe("AppTile", () => {
/> />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
// Wait for RPS room 1 updates to fire act(() =>
const rpsUpdated1 = waitForRps("r1"); dis.dispatch({
dis.dispatch({ action: Action.ViewRoom,
action: Action.ViewRoom, room_id: "r1",
room_id: "r1", }),
}); );
await rpsUpdated1;
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true); await waitFor(() => {
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false); expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
});
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name === "RightPanel.phases") { if (name === "RightPanel.phases") {
@ -263,13 +252,13 @@ describe("AppTile", () => {
} }
return realGetValue(name, roomId); return realGetValue(name, roomId);
}); });
// Wait for RPS room 2 updates to fire
const rpsUpdated2 = waitForRps("r2");
// Switch to room 2 // Switch to room 2
dis.dispatch({ act(() =>
action: Action.ViewRoom, dis.dispatch({
room_id: "r2", action: Action.ViewRoom,
}); room_id: "r2",
}),
);
renderResult.rerender( renderResult.rerender(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<RightPanel <RightPanel
@ -279,10 +268,11 @@ describe("AppTile", () => {
/> />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
await rpsUpdated2;
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false); await waitFor(() => {
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true); 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 () => { it("preserves non-persisted widget on container move", async () => {
@ -345,7 +335,7 @@ describe("AppTile", () => {
let renderResult: RenderResult; let renderResult: RenderResult;
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>; let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(() => { beforeEach(async () => {
renderResult = render( renderResult = render(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} /> <AppTile key={app1.id} app={app1} room={r1} />
@ -353,12 +343,12 @@ describe("AppTile", () => {
); );
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer"); moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
}); });
it("should render", () => { 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 expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
}); });
@ -459,18 +449,19 @@ describe("AppTile", () => {
describe("for a persistent app", () => { describe("for a persistent app", () => {
let renderResult: RenderResult; let renderResult: RenderResult;
beforeEach(() => { beforeEach(async () => {
renderResult = render( renderResult = render(
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} /> <AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
}); });
it("should render", () => { it("should render", async () => {
const { container, asFragment } = renderResult; const { asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(asFragment()).toMatchSnapshot(); 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 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 userEvent from "@testing-library/user-event";
import { mocked, Mocked } from "jest-mock"; import { mocked, Mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
@ -214,9 +214,7 @@ describe("<Pill>", () => {
}); });
// wait for profile query via API // wait for profile query via API
await act(async () => { await flushPromises();
await flushPromises();
});
expect(renderResult.asFragment()).toMatchSnapshot(); expect(renderResult.asFragment()).toMatchSnapshot();
}); });
@ -228,9 +226,7 @@ describe("<Pill>", () => {
}); });
// wait for profile query via API // wait for profile query via API
await act(async () => { await flushPromises();
await flushPromises();
});
expect(renderResult.asFragment()).toMatchSnapshot(); expect(renderResult.asFragment()).toMatchSnapshot();
}); });

View file

@ -60,29 +60,9 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] =
id="1" id="1"
> >
<div <div
class="mx_AppTileBody mx_AppTileBody--large" class="mx_AppTile_persistedWrapper"
> >
<div <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>
</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 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 userEvent from "@testing-library/user-event";
import EmojiPicker from "../../../../../src/components/views/emojipicker/EmojiPicker"; 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 // Apply a filter and assert that the HTML has changed
//@ts-ignore private access //@ts-ignore private access
ref.current!.onChangeFilter("test"); act(() => ref.current!.onChangeFilter("test"));
expect(beforeHtml).not.toEqual(container.innerHTML); expect(beforeHtml).not.toEqual(container.innerHTML);
// Clear the filter and assert that the HTML matches what it was before filtering // Clear the filter and assert that the HTML matches what it was before filtering
//@ts-ignore private access //@ts-ignore private access
ref.current!.onChangeFilter(""); act(() => ref.current!.onChangeFilter(""));
await waitFor(() => expect(beforeHtml).toEqual(container.innerHTML)); 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() }); const ep = new EmojiPicker({ onChoose: (str: string) => false, onFinished: jest.fn() });
//@ts-ignore private access //@ts-ignore private access
ep.onChangeFilter("heart"); act(() => ep.onChangeFilter("heart"));
//@ts-ignore private access //@ts-ignore private access
expect(ep.memoizedDataByCategory["people"][0].shortcodes[0]).toEqual("heart"); 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")!; const [, onGeolocateCallback] = mocked(mockGeolocate.on).mock.calls.find(([event]) => event === "geolocate")!;
// set the location // set the location
onGeolocateCallback(position); act(() => onGeolocateCallback(position));
}; };
const setLocationClick = () => { const setLocationClick = () => {
@ -151,7 +151,7 @@ describe("<LocationShareMenu />", () => {
lngLat: { lng: position.coords.longitude, lat: position.coords.latitude }, lngLat: { lng: position.coords.longitude, lat: position.coords.latitude },
} as unknown as maplibregl.MapMouseEvent; } as unknown as maplibregl.MapMouseEvent;
// set the location // set the location
onMapClickCallback(event); act(() => onMapClickCallback(event));
}; };
const shareTypeLabels: Record<LocationShareType, string> = { const shareTypeLabels: Record<LocationShareType, string> = {

View file

@ -48,6 +48,7 @@ describe("DateSeparator", () => {
<MatrixClientContext.Provider value={mockClient}> <MatrixClientContext.Provider value={mockClient}>
<DateSeparator {...defaultProps} {...props} /> <DateSeparator {...defaultProps} {...props} />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
{ legacyRoot: true },
); );
type TestCase = [string, number, string]; type TestCase = [string, number, string];
@ -264,10 +265,12 @@ describe("DateSeparator", () => {
fireEvent.click(jumpToLastWeekButton); fireEvent.click(jumpToLastWeekButton);
// Expect error to be shown. We have to wait for the UI to transition. // 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 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) => { ].forEach((fakeError) => {
it(`should show error dialog without submit debug logs option when networking error (${fakeError.name}) occurs`, async () => { 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 // Render the component
getComponent(); getComponent();
// Open the jump to date context menu // Open the jump to date context menu
fireEvent.click(screen.getByTestId("jump-to-date-separator-button")); 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"); const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
fireEvent.click(jumpToLastWeekButton); fireEvent.click(jumpToLastWeekButton);
// Expect error to be shown. We have to wait for the UI to transition. // 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. // 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) => { const checkTexts = async (title: string, subTitle: string) => {
screen.getByText(title); await screen.findByText(title);
screen.getByText(subTitle); await screen.findByText(subTitle);
}; };
describe("EncryptionEvent", () => { describe("EncryptionEvent", () => {
@ -120,9 +120,9 @@ describe("EncryptionEvent", () => {
renderEncryptionEvent(client, event); renderEncryptionEvent(client, event);
}); });
it("should show the expected texts", () => { it("should show the expected texts", async () => {
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId); 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 React from "react";
import { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react"; import { act, fireEvent, render, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import { import {
MatrixEvent, MatrixEvent,
Relations, Relations,
@ -226,7 +226,7 @@ describe("MPollBody", () => {
clickOption(renderResult, "pizza"); clickOption(renderResult, "pizza");
// When a new vote from me comes in // 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 // Then the new vote is counted, not the old one
expect(votesCount(renderResult, "pizza")).toBe("0 votes"); expect(votesCount(renderResult, "pizza")).toBe("0 votes");
@ -255,7 +255,7 @@ describe("MPollBody", () => {
clickOption(renderResult, "pizza"); clickOption(renderResult, "pizza");
// When a new vote from someone else comes in // 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 // Then my vote is still for pizza
// NOTE: the new event does not affect the counts for other people - // NOTE: the new event does not affect the counts for other people -
@ -596,11 +596,13 @@ describe("MPollBody", () => {
]; ];
const renderResult = await newMPollBody(votes, ends); const renderResult = await newMPollBody(votes, ends);
expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); await waitFor(() => {
expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes");
expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes");
expect(endedVotesCount(renderResult, "wings")).toBe('<div class="mx_PollOption_winnerIcon"></div>3 votes'); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes");
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 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 () => { it("ignores votes that arrived after the first end poll event", async () => {
@ -890,12 +892,14 @@ async function newMPollBody(
room_id: "#myroom:example.com", room_id: "#myroom:example.com",
content: newPollStart(answers, undefined, disclosed), content: newPollStart(answers, undefined, disclosed),
}); });
const result = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents); const prom = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents);
// flush promises from loading relations
if (waitForResponsesLoad) { 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 { 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 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 { EventTimeline, MatrixEvent, Room, M_TEXT } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -127,6 +127,7 @@ describe("<MPollEndBody />", () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument()); await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument());
await waitForElementToBeRemoved(() => getByRole("progressbar"));
expect(mockClient.fetchRoomEvent).toHaveBeenCalledWith(roomId, pollStartEvent.getId()); 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 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 { Filter, EventTimeline, Room, MatrixEvent, M_POLL_START } from "matrix-js-sdk/src/matrix";
import { PollHistory } from "../../../../../../src/components/views/polls/pollHistory/PollHistory"; import { PollHistory } from "../../../../../../src/components/views/polls/pollHistory/PollHistory";
@ -110,7 +110,7 @@ describe("<PollHistory />", () => {
expect(getByText("Loading polls")).toBeInTheDocument(); expect(getByText("Loading polls")).toBeInTheDocument();
// flush filter creation request // flush filter creation request
await act(flushPromises); await flushPromises();
expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS); expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS);
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true }); expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true });
@ -140,7 +140,7 @@ describe("<PollHistory />", () => {
); );
// flush filter creation request // flush filter creation request
await act(flushPromises); await flushPromises();
// once per page // once per page
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3); 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 () => { it("renders a no polls message when there are no active polls in the room", async () => {
const { getByText } = getComponent(); const { getByText } = getComponent();
await act(flushPromises); await flushPromises();
expect(getByText("There are no active polls in this room")).toBeTruthy(); expect(getByText("There are no active polls in this room")).toBeTruthy();
}); });
@ -199,7 +199,7 @@ describe("<PollHistory />", () => {
.mockReturnValueOnce("test-pagination-token-3"); .mockReturnValueOnce("test-pagination-token-3");
const { getByText } = getComponent(); const { getByText } = getComponent();
await act(flushPromises); await flushPromises();
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1); expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
@ -212,7 +212,7 @@ describe("<PollHistory />", () => {
// load more polls button still in UI, with loader // load more polls button still in UI, with loader
expect(getByText("Load more polls")).toMatchSnapshot(); expect(getByText("Load more polls")).toMatchSnapshot();
await act(flushPromises); await flushPromises();
// no more spinner // no more spinner
expect(getByText("Load more polls")).toMatchSnapshot(); 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" tabindex="0"
> >
<div <div
aria-labelledby=":ra:" aria-labelledby=":rc:"
class="mx_PollListItem_content" class="mx_PollListItem_content"
> >
<span> <span>
@ -116,7 +116,7 @@ exports[`<PollHistory /> renders a list of active polls when there are polls in
tabindex="0" tabindex="0"
> >
<div <div
aria-labelledby=":rg:" aria-labelledby=":rh:"
class="mx_PollListItem_content" class="mx_PollListItem_content"
> >
<span> <span>

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; 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 userEvent from "@testing-library/user-event";
import { Mocked, mocked } from "jest-mock"; import { Mocked, mocked } from "jest-mock";
import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType, Device } from "matrix-js-sdk/src/matrix"; 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} />, { return render(<UserInfo {...defaultProps} {...props} />, {
wrapper: Wrapper, wrapper: Wrapper,
legacyRoot: true,
}); });
}; };
@ -439,7 +440,7 @@ describe("<UserInfo />", () => {
it("renders a device list which can be expanded", async () => { it("renders a device list which can be expanded", async () => {
renderComponent(); renderComponent();
await act(flushPromises); await flushPromises();
// check the button exists with the expected text // check the button exists with the expected text
const devicesButton = screen.getByRole("button", { name: "1 session" }); const devicesButton = screen.getByRole("button", { name: "1 session" });
@ -459,9 +460,9 @@ describe("<UserInfo />", () => {
verificationRequest, verificationRequest,
room: mockRoom, 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(); expect(container).toMatchSnapshot();
}); });
@ -490,7 +491,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted) // check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "1 session" }); const devicesButton = screen.getByRole("button", { name: "1 session" });
@ -538,7 +539,7 @@ describe("<UserInfo />", () => {
} as DeviceVerificationStatus); } as DeviceVerificationStatus);
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted) // check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "1 verified session" }); const devicesButton = screen.getByRole("button", { name: "1 verified session" });
@ -583,7 +584,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true)); mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true));
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await flushPromises();
// the dehydrated device should be shown as an unverified device, which means // the dehydrated device should be shown as an unverified device, which means
// there should now be a button with the device id ... // there should now be a button with the device id ...
@ -618,7 +619,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted) // check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "2 sessions" }); const devicesButton = screen.getByRole("button", { name: "2 sessions" });
@ -653,7 +654,7 @@ describe("<UserInfo />", () => {
room: mockRoom, 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(); expect(container).toMatchSnapshot();
}); });
}); });
@ -666,7 +667,7 @@ describe("<UserInfo />", () => {
it("renders unverified user info", async () => { it("renders unverified user info", async () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false)); mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await flushPromises();
const userHeading = screen.getByRole("heading", { name: /@user:example.com/ }); const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
@ -677,7 +678,7 @@ describe("<UserInfo />", () => {
it("renders verified user info", async () => { it("renders verified user info", async () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, false, false)); mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, false, false));
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await flushPromises();
const userHeading = screen.getByRole("heading", { name: /@user:example.com/ }); 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 () => { it("with unverified user and device, displays button without a label", async () => {
renderComponent(); renderComponent();
await act(flushPromises); await flushPromises();
expect(screen.getByRole("button", { name: device.displayName! })).toBeInTheDocument(); expect(screen.getByRole("button", { name: device.displayName! })).toBeInTheDocument();
expect(screen.queryByText(/trusted/i)).not.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 () => { it("with verified user only, displays button with a 'Not trusted' label", async () => {
renderComponent({ isUserVerified: true }); renderComponent({ isUserVerified: true });
await act(flushPromises); await flushPromises();
const button = screen.getByRole("button", { name: device.displayName }); const button = screen.getByRole("button", { name: device.displayName });
expect(button).toHaveTextContent(`${device.displayName}Not trusted`); 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 () => { it("with verified device only, displays no button without a label", async () => {
setMockDeviceTrust(true); setMockDeviceTrust(true);
renderComponent(); renderComponent();
await act(flushPromises); await flushPromises();
expect(screen.getByText(device.displayName!)).toBeInTheDocument(); expect(screen.getByText(device.displayName!)).toBeInTheDocument();
expect(screen.queryByText(/trusted/)).not.toBeInTheDocument(); expect(screen.queryByText(/trusted/)).not.toBeInTheDocument();
@ -798,7 +799,7 @@ describe("<DeviceItem />", () => {
mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId); mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
mockClient.getUserId.mockReturnValueOnce(defaultUserId); mockClient.getUserId.mockReturnValueOnce(defaultUserId);
renderComponent(); renderComponent();
await act(flushPromises); await flushPromises();
// set trust to be false for isVerified, true for isCrossSigningVerified // set trust to be false for isVerified, true for isCrossSigningVerified
deferred.resolve({ deferred.resolve({
@ -814,7 +815,7 @@ describe("<DeviceItem />", () => {
it("with verified user and device, displays no button and a 'Trusted' label", async () => { it("with verified user and device, displays no button and a 'Trusted' label", async () => {
setMockDeviceTrust(true); setMockDeviceTrust(true);
renderComponent({ isUserVerified: true }); renderComponent({ isUserVerified: true });
await act(flushPromises); await flushPromises();
expect(screen.queryByRole("button")).not.toBeInTheDocument(); expect(screen.queryByRole("button")).not.toBeInTheDocument();
expect(screen.getByText(device.displayName!)).toBeInTheDocument(); expect(screen.getByText(device.displayName!)).toBeInTheDocument();
@ -824,7 +825,7 @@ describe("<DeviceItem />", () => {
it("does not call verifyDevice if client.getUser returns null", async () => { it("does not call verifyDevice if client.getUser returns null", async () => {
mockClient.getUser.mockReturnValueOnce(null); mockClient.getUser.mockReturnValueOnce(null);
renderComponent(); renderComponent();
await act(flushPromises); await flushPromises();
const button = screen.getByRole("button", { name: device.displayName! }); const button = screen.getByRole("button", { name: device.displayName! });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
@ -839,7 +840,7 @@ describe("<DeviceItem />", () => {
// even more mocking // even more mocking
mockClient.isGuest.mockReturnValueOnce(true); mockClient.isGuest.mockReturnValueOnce(true);
renderComponent(); renderComponent();
await act(flushPromises); await flushPromises();
const button = screen.getByRole("button", { name: device.displayName! }); const button = screen.getByRole("button", { name: device.displayName! });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
@ -851,7 +852,7 @@ describe("<DeviceItem />", () => {
it("with display name", async () => { it("with display name", async () => {
const { container } = renderComponent(); const { container } = renderComponent();
await act(flushPromises); await flushPromises();
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
@ -859,7 +860,7 @@ describe("<DeviceItem />", () => {
it("without display name", async () => { it("without display name", async () => {
const device = { deviceId: "deviceId" } as Device; const device = { deviceId: "deviceId" } as Device;
const { container } = renderComponent({ device, userId: defaultUserId }); const { container } = renderComponent({ device, userId: defaultUserId });
await act(flushPromises); await flushPromises();
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
@ -867,7 +868,7 @@ describe("<DeviceItem />", () => {
it("ambiguous display name", async () => { it("ambiguous display name", async () => {
const device = { deviceId: "deviceId", ambiguous: true, displayName: "my display name" }; const device = { deviceId: "deviceId", ambiguous: true, displayName: "my display name" };
const { container } = renderComponent({ device, userId: defaultUserId }); const { container } = renderComponent({ device, userId: defaultUserId });
await act(flushPromises); await flushPromises();
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
@ -1033,9 +1034,7 @@ describe("<UserOptionsSection />", () => {
expect(inviteSpy).toHaveBeenCalledWith([member.userId]); expect(inviteSpy).toHaveBeenCalledWith([member.userId]);
// check that the test error message is displayed // check that the test error message is displayed
await waitFor(() => { await expect(screen.findByText(mockErrorMessage.message)).resolves.toBeInTheDocument();
expect(screen.getByText(mockErrorMessage.message)).toBeInTheDocument();
});
}); });
it("if calling .invite throws something strange, show default error message", async () => { it("if calling .invite throws something strange, show default error message", async () => {
@ -1048,9 +1047,7 @@ describe("<UserOptionsSection />", () => {
await userEvent.click(inviteButton); await userEvent.click(inviteButton);
// check that the default test error message is displayed // check that the default test error message is displayed
await waitFor(() => { await expect(screen.findByText(/operation failed/i)).resolves.toBeInTheDocument();
expect(screen.getByText(/operation failed/i)).toBeInTheDocument();
});
}); });
it.each([ it.each([

View file

@ -260,7 +260,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo); } as EventEncryptionInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises); await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -285,7 +285,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo); } as EventEncryptionInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises); await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -314,7 +314,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo); } as EventEncryptionInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises); await flushPromises();
const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon"); const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
expect(e2eIcons).toHaveLength(1); expect(e2eIcons).toHaveLength(1);
@ -346,7 +346,7 @@ describe("EventTile", () => {
await mxEvent.attemptDecryption(mockCrypto); await mxEvent.attemptDecryption(mockCrypto);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises); await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -400,7 +400,7 @@ describe("EventTile", () => {
const roomContext = getRoomContext(room, {}); const roomContext = getRoomContext(room, {});
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />); const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await act(flushPromises); await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -451,7 +451,7 @@ describe("EventTile", () => {
const roomContext = getRoomContext(room, {}); const roomContext = getRoomContext(room, {});
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />); const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await act(flushPromises); await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); 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 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 { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked, MockedObject } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
@ -361,6 +370,7 @@ describe("MemberList", () => {
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
cleanup();
}); });
const renderComponent = () => { const renderComponent = () => {
@ -397,21 +407,22 @@ describe("MemberList", () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join); jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(room, "canInvite").mockReturnValue(false); jest.spyOn(room, "canInvite").mockReturnValue(false);
renderComponent(); const { findByLabelText } = renderComponent();
await flushPromises();
// button rendered but disabled // 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 () => { 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, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(room, "canInvite").mockReturnValue(true); jest.spyOn(room, "canInvite").mockReturnValue(true);
renderComponent(); const { findByText } = renderComponent();
await flushPromises();
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 () => { 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"; import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
const openStickerPicker = async (): Promise<void> => { const openStickerPicker = async (): Promise<void> => {
await act(async () => { await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("More options")); await userEvent.click(screen.getByLabelText("Sticker"));
await userEvent.click(screen.getByLabelText("Sticker"));
});
}; };
const startVoiceMessage = async (): Promise<void> => { const startVoiceMessage = async (): Promise<void> => {
await act(async () => { await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("More options")); await userEvent.click(screen.getByLabelText("Voice Message"));
await userEvent.click(screen.getByLabelText("Voice Message"));
});
}; };
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => { const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
@ -61,7 +57,7 @@ const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState
MatrixClientPeg.safeGet(), MatrixClientPeg.safeGet(),
state, state,
); );
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording); act(() => SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording));
}; };
const expectVoiceMessageRecordingTriggered = (): void => { 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", () => { describe("for a Room", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli); const room = mkStubRoom("!roomId:server", "Room 1", cli);
@ -185,14 +220,12 @@ describe("MessageComposer", () => {
[true, false].forEach((value: boolean) => { [true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => { describe(`when ${setting} = ${value}`, () => {
beforeEach(async () => { beforeEach(async () => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); await act(() => SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value));
wrapAndRender({ room }); 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) { if (value) {
// eslint-disable-next-line jest/no-conditional-expect // eslint-disable-next-line jest/no-conditional-expect
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument(); expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
@ -205,15 +238,17 @@ describe("MessageComposer", () => {
describe(`and setting ${setting} to ${!value}`, () => { describe(`and setting ${setting} to ${!value}`, () => {
beforeEach(async () => { beforeEach(async () => {
// simulate settings update // simulate settings update
await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value); await act(async () => {
dis.dispatch( await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value);
{ dis.dispatch(
action: Action.SettingUpdated, {
settingName: setting, action: Action.SettingUpdated,
newValue: !value, settingName: setting,
}, newValue: !value,
true, },
); true,
);
});
}); });
it(`should${!value || "not"} display the button`, () => { it(`should${!value || "not"} display the button`, () => {
@ -273,7 +308,7 @@ describe("MessageComposer", () => {
beforeEach(async () => { beforeEach(async () => {
wrapAndRender({ room }, true, true); wrapAndRender({ room }, true, true);
await openStickerPicker(); await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {}); act(() => resizeCallback(UI_EVENTS.Resize, {}));
}); });
it("should close the menu", () => { it("should close the menu", () => {
@ -295,7 +330,7 @@ describe("MessageComposer", () => {
beforeEach(async () => { beforeEach(async () => {
wrapAndRender({ room }, true, false); wrapAndRender({ room }, true, false);
await openStickerPicker(); await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {}); act(() => resizeCallback(UI_EVENTS.Resize, {}));
}); });
it("should close the menu", () => { it("should close the menu", () => {
@ -443,51 +478,6 @@ describe("MessageComposer", () => {
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument(); 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( function wrapAndRender(
@ -529,7 +519,7 @@ function wrapAndRender(
); );
return { return {
rawComponent: getRawComponent(props, roomContext, mockClient), rawComponent: getRawComponent(props, roomContext, mockClient),
renderResult: render(getRawComponent(props, roomContext, mockClient)), renderResult: render(getRawComponent(props, roomContext, mockClient), { legacyRoot: true }),
roomContext, roomContext,
}; };
} }

View file

@ -385,7 +385,7 @@ describe("<SendMessageComposer/>", () => {
it("correctly persists state to and from localStorage", () => { it("correctly persists state to and from localStorage", () => {
const props = { replyToEvent: mockEvent }; const props = { replyToEvent: mockEvent };
const { container, unmount, rerender } = getComponent(props); let { container, unmount } = getComponent(props);
addTextToComposer(container, "Test Text"); addTextToComposer(container, "Test Text");
@ -402,7 +402,7 @@ describe("<SendMessageComposer/>", () => {
}); });
// ensure the correct model is re-loaded // ensure the correct model is re-loaded
rerender(getRawComponent(props)); ({ container, unmount } = getComponent(props));
expect(container.textContent).toBe("Test Text"); expect(container.textContent).toBe("Test Text");
expect(spyDispatcher).toHaveBeenCalledWith({ expect(spyDispatcher).toHaveBeenCalledWith({
action: "reply_to_event", action: "reply_to_event",
@ -413,7 +413,7 @@ describe("<SendMessageComposer/>", () => {
// now try with localStorage wiped out // now try with localStorage wiped out
unmount(); unmount();
localStorage.removeItem(key); localStorage.removeItem(key);
rerender(getRawComponent(props)); ({ container } = getComponent(props));
expect(container.textContent).toBe(""); 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 "@testing-library/jest-dom";
import React from "react"; 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 MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext"; import RoomContext from "../../../../../../src/contexts/RoomContext";
@ -253,9 +253,7 @@ describe("EditWysiwygComposer", () => {
}); });
// Wait for event dispatch to happen // Wait for event dispatch to happen
await act(async () => { await flushPromises();
await flushPromises();
});
// Then we don't get it because we are disabled // Then we don't get it because we are disabled
expect(screen.getByRole("textbox")).not.toHaveFocus(); 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. 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 { MatrixClient, MatrixError, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import React from "react"; import React from "react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@ -48,54 +48,13 @@ describe("AddRemoveThreepids", () => {
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
clearAllModals(); clearAllModals();
cleanup();
}); });
const clientProviderWrapper: React.FC = ({ children }: React.PropsWithChildren) => ( const clientProviderWrapper: React.FC = ({ children }: React.PropsWithChildren) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider> <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 () => { it("should handle no email addresses", async () => {
const { container } = render( const { container } = render(
<AddRemoveThreepids <AddRemoveThreepids
@ -107,6 +66,7 @@ describe("AddRemoveThreepids", () => {
/>, />,
); );
await expect(screen.findByText("Email Address")).resolves.toBeVisible();
expect(container).toMatchSnapshot(); 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); await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" }); const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton); 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); await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" }); const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton); 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); await userEvent.click(countryDropdown);
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" }); const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
await userEvent.click(gbOption); 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); await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible(); 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); await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible(); 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); await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${PHONE1.address}?`)).toBeVisible(); 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/ }); const shareButton = screen.getByRole("button", { name: /Share/ });
await userEvent.click(shareButton); 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/ }); const shareButton = screen.getByRole("button", { name: /Share/ });
await userEvent.click(shareButton); 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/ }); const revokeButton = screen.getByRole("button", { name: /Revoke/ });
await userEvent.click(revokeButton); 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/ }); const revokeButton = screen.getByRole("button", { name: /Revoke/ });
await userEvent.click(revokeButton); 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 <input
autocomplete="email" autocomplete="email"
id="mx_Field_3" id="mx_Field_1"
label="Email Address" label="Email Address"
placeholder="Email Address" placeholder="Email Address"
type="text" type="text"
value="" value=""
/> />
<label <label
for="mx_Field_3" for="mx_Field_1"
> >
Email Address Email Address
</label> </label>
@ -61,14 +61,14 @@ exports[`AddRemoveThreepids should render email addresses 1`] = `
> >
<input <input
autocomplete="email" autocomplete="email"
id="mx_Field_1" id="mx_Field_14"
label="Email Address" label="Email Address"
placeholder="Email Address" placeholder="Email Address"
type="text" type="text"
value="" value=""
/> />
<label <label
for="mx_Field_1" for="mx_Field_14"
> >
Email Address Email Address
</label> </label>
@ -148,14 +148,14 @@ exports[`AddRemoveThreepids should render phone numbers 1`] = `
</span> </span>
<input <input
autocomplete="tel-national" autocomplete="tel-national"
id="mx_Field_2" id="mx_Field_15"
label="Phone Number" label="Phone Number"
placeholder="Phone Number" placeholder="Phone Number"
type="text" type="text"
value="" value=""
/> />
<label <label
for="mx_Field_2" for="mx_Field_15"
> >
Phone Number Phone Number
</label> </label>

View file

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

View file

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

View file

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

View file

@ -388,7 +388,7 @@ exports[`<SessionManagerTab /> goes to filtered list from security recommendatio
> >
<input <input
aria-label="Select all" aria-label="Select all"
aria-labelledby=":r4e:" aria-labelledby=":r3s:"
data-testid="device-select-all-checkbox" data-testid="device-select-all-checkbox"
id="device-select-all-checkbox" id="device-select-all-checkbox"
type="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 React, { ComponentProps } from "react";
import { mocked, Mocked } from "jest-mock"; 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 { TypedEventEmitter, IMyDevice, MatrixClient, Device } from "matrix-js-sdk/src/matrix";
import { VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api"; import { VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api";
@ -63,9 +63,7 @@ describe("VerificationRequestToast", () => {
otherDeviceId, otherDeviceId,
}); });
const result = renderComponent({ request }); const result = renderComponent({ request });
await act(async () => { await flushPromises();
await flushPromises();
});
expect(result.container).toMatchSnapshot(); expect(result.container).toMatchSnapshot();
}); });
@ -76,9 +74,7 @@ describe("VerificationRequestToast", () => {
otherUserId, otherUserId,
}); });
const result = renderComponent({ request }); const result = renderComponent({ request });
await act(async () => { await flushPromises();
await flushPromises();
});
expect(result.container).toMatchSnapshot(); expect(result.container).toMatchSnapshot();
}); });
@ -89,9 +85,7 @@ describe("VerificationRequestToast", () => {
otherUserId, otherUserId,
}); });
renderComponent({ request, toastKey: "testKey" }); renderComponent({ request, toastKey: "testKey" });
await act(async () => { await flushPromises();
await flushPromises();
});
const dismiss = jest.spyOn(ToastStore.sharedInstance(), "dismissToast"); const dismiss = jest.spyOn(ToastStore.sharedInstance(), "dismissToast");
Object.defineProperty(request, "accepting", { value: true }); 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(); expect(renderResult.baseElement).toMatchSnapshot();
}); });

View file

@ -21,7 +21,7 @@ describe("requestMediaPermissions", () => {
const itShouldLogTheErrorAndShowTheNoMediaPermissionsModal = () => { const itShouldLogTheErrorAndShowTheNoMediaPermissionsModal = () => {
it("should log the error and show the »No media permissions« modal", async () => { it("should log the error and show the »No media permissions« modal", async () => {
expect(logger.log).toHaveBeenCalledWith("Failed to list userMedia devices", error); 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> </p>
<div <div
class="mx_Flex mx_ErrorView_buttons" 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 <button
class="_button_i91xf_17 _has-icon_i91xf_66" class="_button_i91xf_17 _has-icon_i91xf_66"
@ -152,6 +153,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
</h2> </h2>
<div <div
class="mx_Flex" 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 <a
class="_button_i91xf_17 _has-icon_i91xf_66" class="_button_i91xf_17 _has-icon_i91xf_66"
@ -221,6 +223,7 @@ exports[`showIncompatibleBrowser should match snapshot 1`] = `
</h2> </h2>
<div <div
class="mx_Flex" 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 <a
href="https://apps.apple.com/app/vector/id1083446067" 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. 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() { function setUpMatrixChatDiv() {
document.getElementById("matrixchat")?.remove(); document.getElementById("matrixchat")?.remove();
@ -19,6 +24,7 @@ describe("showIncompatibleBrowser", () => {
it("should match snapshot", async () => { it("should match snapshot", async () => {
await showIncompatibleBrowser(jest.fn()); await showIncompatibleBrowser(jest.fn());
await screen.findByText("Element does not support this browser");
expect(document.getElementById("matrixchat")).toMatchSnapshot(); expect(document.getElementById("matrixchat")).toMatchSnapshot();
}); });
}); });
@ -28,6 +34,19 @@ describe("showError", () => {
it("should match snapshot", async () => { it("should match snapshot", async () => {
await showError("Error title", ["msg1", "msg2"]); await showError("Error title", ["msg1", "msg2"]);
await screen.findByText("Error title");
expect(document.getElementById("matrixchat")).toMatchSnapshot(); 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 () => { beforeEach(async () => {
renderResult = render(<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={preRecording} />); renderResult = render(<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={preRecording} />);
await act(async () => { await flushPromises();
flushPromises();
});
}); });
it("should match the snapshot", () => { 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 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 userEvent from "@testing-library/user-event";
import { ClientEvent, MatrixClient, MatrixEvent, SyncState } from "matrix-js-sdk/src/matrix"; import { ClientEvent, MatrixClient, MatrixEvent, SyncState } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
@ -61,9 +61,7 @@ describe("VoiceBroadcastRecordingPip", () => {
jest.spyOn(recording, "pause"); jest.spyOn(recording, "pause");
jest.spyOn(recording, "resume"); jest.spyOn(recording, "resume");
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />); renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
await act(async () => { await flushPromises();
flushPromises();
});
}; };
const itShouldShowTheBroadcastRoom = () => { const itShouldShowTheBroadcastRoom = () => {
@ -152,8 +150,9 @@ describe("VoiceBroadcastRecordingPip", () => {
describe("and clicking the stop button", () => { describe("and clicking the stop button", () => {
beforeEach(async () => { beforeEach(async () => {
await userEvent.click(screen.getByLabelText("Stop Recording")); await userEvent.click(screen.getByLabelText("Stop Recording"));
await screen.findByText("Stop live broadcasting?");
// modal rendering has some weird sleeps // modal rendering has some weird sleeps
await sleep(100); await sleep(200);
}); });
it("should display the confirm end dialog", () => { it("should display the confirm end dialog", () => {