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
44 changed files with 719 additions and 731 deletions

View File

@@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
import "core-js/stable/structured-clone";
import "fake-indexeddb/auto";
import React, { ComponentProps } from "react";
import { fireEvent, render, RenderResult, screen, waitFor, within } from "jest-matrix-react";
import { fireEvent, render, RenderResult, screen, waitFor, within, act } from "jest-matrix-react";
import fetchMock from "fetch-mock-jest";
import { Mocked, mocked } from "jest-mock";
import { ClientEvent, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
@@ -163,7 +163,7 @@ describe("<MatrixChat />", () => {
let initPromise: Promise<void> | undefined;
let defaultProps: ComponentProps<typeof MatrixChat>;
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) =>
render(<MatrixChat {...defaultProps} {...props} />);
render(<MatrixChat {...defaultProps} {...props} />, { legacyRoot: true });
// make test results readable
filterConsole(
@@ -201,7 +201,7 @@ describe("<MatrixChat />", () => {
// we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Syncing…");
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
await act(() => client.emit(ClientEvent.Sync, SyncState.Prepared, null));
}
// let things settle
@@ -263,7 +263,7 @@ describe("<MatrixChat />", () => {
// emit a loggedOut event so that all of the Store singletons forget about their references to the mock client
// (must be sync otherwise the next test will start before it happens)
defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true);
act(() => defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true));
localStorage.clear();
});
@@ -328,7 +328,7 @@ describe("<MatrixChat />", () => {
expect(within(dialog).getByText(errorMessage)).toBeInTheDocument();
// just check we're back on welcome page
await expect(await screen.findByTestId("mx_welcome_screen")).toBeInTheDocument();
await expect(screen.findByTestId("mx_welcome_screen")).resolves.toBeInTheDocument();
};
beforeEach(() => {
@@ -956,9 +956,11 @@ describe("<MatrixChat />", () => {
await screen.findByText("Powered by Matrix");
// go to login page
defaultDispatcher.dispatch({
action: "start_login",
});
act(() =>
defaultDispatcher.dispatch({
action: "start_login",
}),
);
await flushPromises();
@@ -1126,9 +1128,11 @@ describe("<MatrixChat />", () => {
await getComponentAndLogin();
bootstrapDeferred.resolve();
act(() => bootstrapDeferred.resolve());
await expect(await screen.findByRole("heading", { name: "You're in", level: 1 })).toBeInTheDocument();
await expect(
screen.findByRole("heading", { name: "You're in", level: 1 }),
).resolves.toBeInTheDocument();
});
});
});
@@ -1397,7 +1401,9 @@ describe("<MatrixChat />", () => {
function simulateSessionLockClaim() {
localStorage.setItem("react_sdk_session_lock_claimant", "testtest");
window.dispatchEvent(new StorageEvent("storage", { key: "react_sdk_session_lock_claimant" }));
act(() =>
window.dispatchEvent(new StorageEvent("storage", { key: "react_sdk_session_lock_claimant" })),
);
}
it("after a session is restored", async () => {

View File

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

View File

@@ -23,14 +23,22 @@ import {
} from "matrix-js-sdk/src/matrix";
import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import {
fireEvent,
render,
screen,
RenderResult,
waitForElementToBeRemoved,
waitFor,
act,
cleanup,
} from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import {
stubClient,
mockPlatformPeg,
unmockPlatformPeg,
wrapInMatrixClientContext,
flushPromises,
mkEvent,
setupAsyncStoreWithClient,
@@ -45,7 +53,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { Action } from "../../../../src/dispatcher/actions";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
import { RoomView as _RoomView } from "../../../../src/components/structures/RoomView";
import { RoomView } from "../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
@@ -64,8 +72,7 @@ import WidgetStore from "../../../../src/stores/WidgetStore";
import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRoomErrorPayload";
import { SearchScope } from "../../../../src/Searching";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
const RoomView = wrapInMatrixClientContext(_RoomView);
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
describe("RoomView", () => {
let cli: MockedObject<MatrixClient>;
@@ -106,9 +113,10 @@ describe("RoomView", () => {
afterEach(() => {
unmockPlatformPeg();
jest.clearAllMocks();
cleanup();
});
const mountRoomView = async (ref?: RefObject<_RoomView>): Promise<RenderResult> => {
const mountRoomView = async (ref?: RefObject<RoomView>): Promise<RenderResult> => {
if (stores.roomViewStore.getRoomId() !== room.roomId) {
const switchedRoom = new Promise<void>((resolve) => {
const subFn = () => {
@@ -120,26 +128,30 @@ describe("RoomView", () => {
stores.roomViewStore.on(UPDATE_EVENT, subFn);
});
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: undefined,
});
act(() =>
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: undefined,
}),
);
await switchedRoom;
}
const roomView = render(
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
wrappedRef={ref as any}
/>
</SDKContext.Provider>,
<MatrixClientContext.Provider value={cli}>
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
ref={ref}
/>
</SDKContext.Provider>
</MatrixClientContext.Provider>,
);
await flushPromises();
return roomView;
@@ -167,22 +179,24 @@ describe("RoomView", () => {
}
const roomView = render(
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
onRegistered={jest.fn()}
/>
</SDKContext.Provider>,
<MatrixClientContext.Provider value={cli}>
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
onRegistered={jest.fn()}
/>
</SDKContext.Provider>
</MatrixClientContext.Provider>,
);
await flushPromises();
return roomView;
};
const getRoomViewInstance = async (): Promise<_RoomView> => {
const ref = createRef<_RoomView>();
const getRoomViewInstance = async (): Promise<RoomView> => {
const ref = createRef<RoomView>();
await mountRoomView(ref);
return ref.current!;
};
@@ -193,7 +207,7 @@ describe("RoomView", () => {
});
describe("when there is an old room", () => {
let instance: _RoomView;
let instance: RoomView;
let oldRoom: Room;
beforeEach(async () => {
@@ -217,11 +231,11 @@ describe("RoomView", () => {
describe("and feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
instance.setState({ msc3946ProcessDynamicPredecessor: true });
act(() => instance.setState({ msc3946ProcessDynamicPredecessor: true }));
});
afterEach(() => {
instance.setState({ msc3946ProcessDynamicPredecessor: false });
act(() => instance.setState({ msc3946ProcessDynamicPredecessor: false }));
});
it("should pass the setting to findPredecessor", async () => {
@@ -252,15 +266,17 @@ describe("RoomView", () => {
cli.isRoomEncrypted.mockReturnValue(true);
// and fake an encryption event into the room to prompt it to re-check
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]);
await act(() =>
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]),
);
// URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false);
@@ -270,7 +286,7 @@ describe("RoomView", () => {
const roomViewInstance = await getRoomViewInstance();
const oldTimeline = roomViewInstance.state.liveTimeline;
room.getUnfilteredTimelineSet().resetLiveTimeline();
act(() => room.getUnfilteredTimelineSet().resetLiveTimeline());
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
});
@@ -287,7 +303,7 @@ describe("RoomView", () => {
await renderRoomView();
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
cli.emit(ClientEvent.Room, room);
act(() => cli.emit(ClientEvent.Room, room));
// called again after room event
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2);
@@ -429,6 +445,194 @@ describe("RoomView", () => {
});
});
it("should show error view if failed to look up room alias", async () => {
const { asFragment, findByText } = await renderRoomView(false);
act(() =>
defaultDispatcher.dispatch<ViewRoomErrorPayload>({
action: Action.ViewRoomError,
room_alias: "#addy:server",
room_id: null,
err: new MatrixError({ errcode: "M_NOT_FOUND" }),
}),
);
await emitPromise(stores.roomViewStore, UPDATE_EVENT);
await findByText("Are you sure you're at the right place?");
expect(asFragment()).toMatchSnapshot();
});
describe("knock rooms", () => {
const client = createTestClient();
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
jest.spyOn(defaultDispatcher, "dispatch");
});
it("allows to request to join", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "knockRoom").mockResolvedValue({ room_id: room.roomId });
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
await untilDispatch(Action.SubmitAskToJoin, defaultDispatcher);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "submit_ask_to_join",
roomId: room.roomId,
opts: { reason: undefined },
});
});
it("allows to cancel a join request", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "leave").mockResolvedValue({});
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Knock);
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
await untilDispatch(Action.CancelAskToJoin, defaultDispatcher);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "cancel_ask_to_join",
roomId: room.roomId,
});
});
});
it("should close search results when edit is clicked", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.Room,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await prom;
});
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
rooms.set(room2.roomId, room2);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.All,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room2.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = untilDispatch(Action.ViewRoom, defaultDispatcher);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
});
it("fires Action.RoomLoaded", async () => {
jest.spyOn(defaultDispatcher, "dispatch");
await mountRoomView();
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
});
describe("when there is a RoomView", () => {
const widget1Id = "widget1";
const widget2Id = "widget2";
@@ -514,184 +718,4 @@ describe("RoomView", () => {
});
});
});
it("should show error view if failed to look up room alias", async () => {
const { asFragment, findByText } = await renderRoomView(false);
defaultDispatcher.dispatch<ViewRoomErrorPayload>({
action: Action.ViewRoomError,
room_alias: "#addy:server",
room_id: null,
err: new MatrixError({ errcode: "M_NOT_FOUND" }),
});
await emitPromise(stores.roomViewStore, UPDATE_EVENT);
await findByText("Are you sure you're at the right place?");
expect(asFragment()).toMatchSnapshot();
});
describe("knock rooms", () => {
const client = createTestClient();
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
jest.spyOn(defaultDispatcher, "dispatch");
});
it("allows to request to join", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "knockRoom").mockResolvedValue({ room_id: room.roomId });
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
await untilDispatch(Action.SubmitAskToJoin, defaultDispatcher);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "submit_ask_to_join",
roomId: room.roomId,
opts: { reason: undefined },
});
});
it("allows to cancel a join request", async () => {
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
jest.spyOn(client, "leave").mockResolvedValue({});
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Knock);
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
await untilDispatch(Action.CancelAskToJoin, defaultDispatcher);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "cancel_ask_to_join",
roomId: room.roomId,
});
});
});
it("should close search results when edit is clicked", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<_RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
// @ts-ignore - triggering a search organically is a lot of work
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.Room,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
});
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await prom;
});
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
rooms.set(room2.roomId, room2);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<_RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
// @ts-ignore - triggering a search organically is a lot of work
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.All,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room2.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
});
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = untilDispatch(Action.ViewRoom, defaultDispatcher);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
});
it("fires Action.RoomLoaded", async () => {
jest.spyOn(defaultDispatcher, "dispatch");
await mountRoomView();
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
});
});

View File

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

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render, waitFor, screen } from "jest-matrix-react";
import { render, waitFor, screen, act } from "jest-matrix-react";
import {
ReceiptType,
EventTimelineSet,
@@ -205,8 +205,10 @@ describe("TimelinePanel", () => {
manageReadReceipts={true}
ref={ref}
/>,
{ legacyRoot: true },
);
await flushPromises();
await waitFor(() => expect(ref.current).toBeTruthy());
timelinePanel = ref.current!;
};
@@ -255,14 +257,16 @@ describe("TimelinePanel", () => {
describe("and reading the timeline", () => {
beforeEach(async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
await flushPromises();
await act(async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
await flushPromises();
// @ts-ignore
await timelinePanel.sendReadReceipts();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker();
// @ts-ignore
await timelinePanel.sendReadReceipts();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker();
});
});
it("should send a fully read marker and a public receipt", async () => {
@@ -276,7 +280,7 @@ describe("TimelinePanel", () => {
client.setRoomReadMarkers.mockClear();
// @ts-ignore Simulate user activity by calling updateReadMarker on the TimelinePanel.
await timelinePanel.updateReadMarker();
await act(() => timelinePanel.updateReadMarker());
});
it("should not send receipts again", () => {
@@ -315,7 +319,7 @@ describe("TimelinePanel", () => {
it("should send a fully read marker and a private receipt", async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
act(() => timelineSet.addLiveEvent(ev1, {}));
await flushPromises();
// @ts-ignore
@@ -326,6 +330,7 @@ describe("TimelinePanel", () => {
// Expect the fully_read marker not to be send yet
expect(client.setRoomReadMarkers).not.toHaveBeenCalled();
await flushPromises();
client.sendReadReceipt.mockClear();
// @ts-ignore simulate user activity
@@ -334,7 +339,7 @@ describe("TimelinePanel", () => {
// It should not send the receipt again.
expect(client.sendReadReceipt).not.toHaveBeenCalledWith(ev1, ReceiptType.ReadPrivate);
// Expect the fully_read marker to be sent after user activity.
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev1.getId());
await waitFor(() => expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev1.getId()));
});
});
});
@@ -361,11 +366,11 @@ describe("TimelinePanel", () => {
it("should send receipts but no fully_read when reading the thread timeline", async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(threadEv1, {});
act(() => timelineSet.addLiveEvent(threadEv1, {}));
await flushPromises();
// @ts-ignore
await timelinePanel.sendReadReceipts();
await act(() => timelinePanel.sendReadReceipts());
// fully_read is not supported for threads per spec
expect(client.setRoomReadMarkers).not.toHaveBeenCalled();
@@ -1021,7 +1026,7 @@ describe("TimelinePanel", () => {
await waitFor(() => expectEvents(container, [events[1]]));
});
defaultDispatcher.fire(Action.DumpDebugLogs);
act(() => defaultDispatcher.fire(Action.DumpDebugLogs));
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(expect.stringContaining("TimelinePanel(Room): Debugging info for roomId")),

View File

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