Better error handling in jump to date (#10405)
- Friendly error messages with details - Add a way to submit debug logs for actual errors (non-networking errors) - Don't jump someone back to a room they already navigated away from. Fixes bug mentioned in https://github.com/vector-im/element-web/issues/21263#issuecomment-1056809714
This commit is contained in:
parent
1af71089dd
commit
e5f06df3f7
11 changed files with 424 additions and 83 deletions
|
@ -149,13 +149,18 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
return this.appendDialogAsync<C>(Promise.resolve(Element), props, className);
|
return this.appendDialogAsync<C>(Promise.resolve(Element), props, className);
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeCurrentModal(reason: string): void {
|
/**
|
||||||
|
* @param reason either "backgroundClick" or undefined
|
||||||
|
* @return whether a modal was closed
|
||||||
|
*/
|
||||||
|
public closeCurrentModal(reason?: string): boolean {
|
||||||
const modal = this.getCurrentModal();
|
const modal = this.getCurrentModal();
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
modal.closeReason = reason;
|
modal.closeReason = reason;
|
||||||
modal.close();
|
modal.close();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildModal<C extends ComponentType>(
|
private buildModal<C extends ComponentType>(
|
||||||
|
@ -346,6 +351,8 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reRender(): Promise<void> {
|
private async reRender(): Promise<void> {
|
||||||
|
// TODO: We should figure out how to remove this weird sleep. It also makes testing harder
|
||||||
|
//
|
||||||
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
|
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
|
|
||||||
|
|
|
@ -18,16 +18,19 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/http-api";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { formatFullDateNoTime } from "../../../DateUtils";
|
import { formatFullDateNoDay, formatFullDateNoTime } from "../../../DateUtils";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import BugReportDialog from "../dialogs/BugReportDialog";
|
||||||
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { contextMenuBelow } from "../rooms/RoomTile";
|
import { contextMenuBelow } from "../rooms/RoomTile";
|
||||||
import { ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
import { ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
|
@ -36,6 +39,7 @@ import IconizedContextMenu, {
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import JumpToDatePicker from "./JumpToDatePicker";
|
import JumpToDatePicker from "./JumpToDatePicker";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
|
||||||
function getDaysArray(): string[] {
|
function getDaysArray(): string[] {
|
||||||
return [_t("Sunday"), _t("Monday"), _t("Tuesday"), _t("Wednesday"), _t("Thursday"), _t("Friday"), _t("Saturday")];
|
return [_t("Sunday"), _t("Monday"), _t("Tuesday"), _t("Wednesday"), _t("Thursday"), _t("Friday"), _t("Saturday")];
|
||||||
|
@ -76,7 +80,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||||
if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef);
|
if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onContextMenuOpenClick = (e: React.MouseEvent): void => {
|
private onContextMenuOpenClick = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const target = e.target as HTMLButtonElement;
|
const target = e.target as HTMLButtonElement;
|
||||||
|
@ -118,12 +122,12 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private pickDate = async (inputTimestamp: number | string | Date): Promise<void> => {
|
private pickDate = async (inputTimestamp: number | string | Date): Promise<void> => {
|
||||||
const unixTimestamp = new Date(inputTimestamp).getTime();
|
const unixTimestamp = new Date(inputTimestamp).getTime();
|
||||||
|
const roomIdForJumpRequest = this.props.roomId;
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
try {
|
try {
|
||||||
const roomId = this.props.roomId;
|
const cli = MatrixClientPeg.get();
|
||||||
const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent(
|
const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent(
|
||||||
roomId,
|
roomIdForJumpRequest,
|
||||||
unixTimestamp,
|
unixTimestamp,
|
||||||
Direction.Forward,
|
Direction.Forward,
|
||||||
);
|
);
|
||||||
|
@ -132,28 +136,113 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||||
`found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp} (looking forward)`,
|
`found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp} (looking forward)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
// Only try to navigate to the room if the user is still viewing the same
|
||||||
|
// room. We don't want to jump someone back to a room after a slow request
|
||||||
|
// if they've already navigated away to another room.
|
||||||
|
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||||
|
if (currentRoomId === roomIdForJumpRequest) {
|
||||||
|
dispatcher.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
room_id: roomId,
|
room_id: roomIdForJumpRequest,
|
||||||
metricsTrigger: undefined, // room doesn't change
|
metricsTrigger: undefined, // room doesn't change
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} else {
|
||||||
const code = e.errcode || e.statusCode;
|
logger.debug(
|
||||||
// only show the dialog if failing for something other than a network error
|
`No longer navigating to date in room (jump to date) because the user already switched ` +
|
||||||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
`to another room: currentRoomId=${currentRoomId}, roomIdForJumpRequest=${roomIdForJumpRequest}`,
|
||||||
// detached queue and we show the room status bar to allow retry
|
);
|
||||||
if (typeof code !== "undefined") {
|
}
|
||||||
// display error message stating you couldn't delete this.
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Error occured while trying to find event in ${roomIdForJumpRequest} ` +
|
||||||
|
`at timestamp=${unixTimestamp}:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only display an error if the user is still viewing the same room. We
|
||||||
|
// don't want to worry someone about an error in a room they no longer care
|
||||||
|
// about after a slow request if they've already navigated away to another
|
||||||
|
// room.
|
||||||
|
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||||
|
if (currentRoomId === roomIdForJumpRequest) {
|
||||||
|
let friendlyErrorMessage = "An error occured while trying to find and jump to the given date.";
|
||||||
|
let submitDebugLogsContent: JSX.Element = <></>;
|
||||||
|
if (err instanceof ConnectionError) {
|
||||||
|
friendlyErrorMessage = _t(
|
||||||
|
"A network error occurred while trying to find and jump to the given date. " +
|
||||||
|
"Your homeserver might be down or there was just a temporary problem with " +
|
||||||
|
"your internet connection. Please try again. If this continues, please " +
|
||||||
|
"contact your homeserver administrator.",
|
||||||
|
);
|
||||||
|
} else if (err instanceof MatrixError) {
|
||||||
|
if (err?.errcode === "M_NOT_FOUND") {
|
||||||
|
friendlyErrorMessage = _t(
|
||||||
|
"We were unable to find an event looking forwards from %(dateString)s. " +
|
||||||
|
"Try choosing an earlier date.",
|
||||||
|
{ dateString: formatFullDateNoDay(new Date(unixTimestamp)) },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
friendlyErrorMessage = _t("Server returned %(statusCode)s with error code %(errorCode)s", {
|
||||||
|
statusCode: err?.httpStatus || _t("unknown status code"),
|
||||||
|
errorCode: err?.errcode || _t("unavailable"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (err instanceof HTTPError) {
|
||||||
|
friendlyErrorMessage = err.message;
|
||||||
|
} else {
|
||||||
|
// We only give the option to submit logs for actual errors, not network problems.
|
||||||
|
submitDebugLogsContent = (
|
||||||
|
<p>
|
||||||
|
{_t(
|
||||||
|
"Please submit <debugLogsLink>debug logs</debugLogsLink> to help us " +
|
||||||
|
"track down the problem.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
debugLogsLink: (sub) => (
|
||||||
|
<AccessibleButton
|
||||||
|
// This is by default a `<div>` which we
|
||||||
|
// can't nest within a `<p>` here so update
|
||||||
|
// this to a be a inline anchor element.
|
||||||
|
element="a"
|
||||||
|
kind="link"
|
||||||
|
onClick={() => this.onBugReport(err instanceof Error ? err : undefined)}
|
||||||
|
data-testid="jump-to-date-error-submit-debug-logs-button"
|
||||||
|
>
|
||||||
|
{sub}
|
||||||
|
</AccessibleButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Unable to find event at that date"),
|
||||||
description: _t("Unable to find event at that date. (%(code)s)", { code }),
|
description: (
|
||||||
|
<div data-testid="jump-to-date-error-content">
|
||||||
|
<p>{friendlyErrorMessage}</p>
|
||||||
|
{submitDebugLogsContent}
|
||||||
|
<details>
|
||||||
|
<summary>{_t("Error details")}</summary>
|
||||||
|
<p>{String(err)}</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onBugReport = (err?: Error): void => {
|
||||||
|
Modal.createDialog(BugReportDialog, {
|
||||||
|
error: err,
|
||||||
|
initialText: "Error occured while using jump to date #jump-to-date",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onLastWeekClicked = (): void => {
|
private onLastWeekClicked = (): void => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setDate(date.getDate() - 7);
|
date.setDate(date.getDate() - 7);
|
||||||
|
@ -189,11 +278,20 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||||
onFinished={this.onContextMenuCloseClick}
|
onFinished={this.onContextMenuCloseClick}
|
||||||
>
|
>
|
||||||
<IconizedContextMenuOptionList first>
|
<IconizedContextMenuOptionList first>
|
||||||
<IconizedContextMenuOption label={_t("Last week")} onClick={this.onLastWeekClicked} />
|
<IconizedContextMenuOption
|
||||||
<IconizedContextMenuOption label={_t("Last month")} onClick={this.onLastMonthClicked} />
|
label={_t("Last week")}
|
||||||
|
onClick={this.onLastWeekClicked}
|
||||||
|
data-testid="jump-to-date-last-week"
|
||||||
|
/>
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
label={_t("Last month")}
|
||||||
|
onClick={this.onLastMonthClicked}
|
||||||
|
data-testid="jump-to-date-last-month"
|
||||||
|
/>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
label={_t("The beginning of the room")}
|
label={_t("The beginning of the room")}
|
||||||
onClick={this.onTheBeginningClicked}
|
onClick={this.onTheBeginningClicked}
|
||||||
|
data-testid="jump-to-date-beginning"
|
||||||
/>
|
/>
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
|
|
||||||
|
@ -207,6 +305,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<ContextMenuTooltipButton
|
<ContextMenuTooltipButton
|
||||||
className="mx_DateSeparator_jumpToDateMenu mx_DateSeparator_dateContent"
|
className="mx_DateSeparator_jumpToDateMenu mx_DateSeparator_dateContent"
|
||||||
|
data-testid="jump-to-date-separator-button"
|
||||||
onClick={this.onContextMenuOpenClick}
|
onClick={this.onContextMenuOpenClick}
|
||||||
isExpanded={!!this.state.contextMenuPosition}
|
isExpanded={!!this.state.contextMenuPosition}
|
||||||
title={_t("Jump to date")}
|
title={_t("Jump to date")}
|
||||||
|
|
|
@ -2353,7 +2353,14 @@
|
||||||
"Saturday": "Saturday",
|
"Saturday": "Saturday",
|
||||||
"Today": "Today",
|
"Today": "Today",
|
||||||
"Yesterday": "Yesterday",
|
"Yesterday": "Yesterday",
|
||||||
"Unable to find event at that date. (%(code)s)": "Unable to find event at that date. (%(code)s)",
|
"A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.",
|
||||||
|
"We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.",
|
||||||
|
"Server returned %(statusCode)s with error code %(errorCode)s": "Server returned %(statusCode)s with error code %(errorCode)s",
|
||||||
|
"unknown status code": "unknown status code",
|
||||||
|
"unavailable": "unavailable",
|
||||||
|
"Please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "Please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
|
||||||
|
"Unable to find event at that date": "Unable to find event at that date",
|
||||||
|
"Error details": "Error details",
|
||||||
"Last week": "Last week",
|
"Last week": "Last week",
|
||||||
"Last month": "Last month",
|
"Last month": "Last month",
|
||||||
"The beginning of the room": "The beginning of the room",
|
"The beginning of the room": "The beginning of the room",
|
||||||
|
|
|
@ -22,8 +22,13 @@ 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 { filterConsole, flushPromisesWithFakeTimers, stubClient } from "../../../test-utils";
|
import {
|
||||||
import Modal from "../../../../src/Modal";
|
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", () => ({
|
||||||
|
@ -55,11 +60,6 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForDialog = async (): Promise<void> => {
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
};
|
|
||||||
|
|
||||||
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
|
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
|
||||||
it("should close the dialog and show the password input", () => {
|
it("should close the dialog and show the password input", () => {
|
||||||
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||||
|
@ -88,9 +88,9 @@ describe("<ForgotPassword>", () => {
|
||||||
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
// clean up modals
|
// clean up modals
|
||||||
Modal.closeCurrentModal("force");
|
await clearAllModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
@ -322,7 +322,9 @@ describe("<ForgotPassword>", () => {
|
||||||
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 waitForDialog();
|
await waitEnoughCyclesForModal({
|
||||||
|
useFakeTimers: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send the new password and show the click validation link dialog", () => {
|
it("should send the new password and show the click validation link dialog", () => {
|
||||||
|
@ -350,7 +352,9 @@ describe("<ForgotPassword>", () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
|
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
|
||||||
});
|
});
|
||||||
await waitForDialog();
|
await waitEnoughCyclesForModal({
|
||||||
|
useFakeTimers: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldCloseTheDialogAndShowThePasswordInput();
|
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||||
|
@ -359,7 +363,9 @@ describe("<ForgotPassword>", () => {
|
||||||
describe("and dismissing the dialog", () => {
|
describe("and dismissing the dialog", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await click(screen.getByLabelText("Close dialog"));
|
await click(screen.getByLabelText("Close dialog"));
|
||||||
await waitForDialog();
|
await waitEnoughCyclesForModal({
|
||||||
|
useFakeTimers: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldCloseTheDialogAndShowThePasswordInput();
|
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||||
|
@ -368,7 +374,9 @@ describe("<ForgotPassword>", () => {
|
||||||
describe("and clicking »Re-enter email address«", () => {
|
describe("and clicking »Re-enter email address«", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await click(screen.getByText("Re-enter email address"));
|
await click(screen.getByText("Re-enter email address"));
|
||||||
await waitForDialog();
|
await waitEnoughCyclesForModal({
|
||||||
|
useFakeTimers: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should close the dialog and go back to the email input", () => {
|
it("should close the dialog and go back to the email input", () => {
|
||||||
|
@ -400,7 +408,9 @@ 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 waitForDialog();
|
await waitEnoughCyclesForModal({
|
||||||
|
useFakeTimers: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the sign out warning dialog", async () => {
|
it("should show the sign out warning dialog", async () => {
|
||||||
|
|
|
@ -16,13 +16,24 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { render } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { TimestampToEventResponse } from "matrix-js-sdk/src/client";
|
||||||
|
import { ConnectionError, HTTPError, MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
|
||||||
|
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
|
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
import { formatFullDateNoTime } from "../../../../src/DateUtils";
|
import { formatFullDateNoTime } from "../../../../src/DateUtils";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../src/settings/UIFeature";
|
import { UIFeature } from "../../../../src/settings/UIFeature";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import { getMockClientWithEventEmitter } from "../../../test-utils";
|
import {
|
||||||
|
clearAllModals,
|
||||||
|
flushPromisesWithFakeTimers,
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
waitEnoughCyclesForModal,
|
||||||
|
} from "../../../test-utils";
|
||||||
import DateSeparator from "../../../../src/components/views/messages/DateSeparator";
|
import DateSeparator from "../../../../src/components/views/messages/DateSeparator";
|
||||||
|
|
||||||
jest.mock("../../../../src/settings/SettingsStore");
|
jest.mock("../../../../src/settings/SettingsStore");
|
||||||
|
@ -31,21 +42,16 @@ describe("DateSeparator", () => {
|
||||||
const HOUR_MS = 3600000;
|
const HOUR_MS = 3600000;
|
||||||
const DAY_MS = HOUR_MS * 24;
|
const DAY_MS = HOUR_MS * 24;
|
||||||
// Friday Dec 17 2021, 9:09am
|
// Friday Dec 17 2021, 9:09am
|
||||||
const now = "2021-12-17T08:09:00.000Z";
|
const nowDate = new Date("2021-12-17T08:09:00.000Z");
|
||||||
const nowMs = 1639728540000;
|
const roomId = "!unused:example.org";
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
ts: nowMs,
|
ts: nowDate.getTime(),
|
||||||
now,
|
roomId,
|
||||||
roomId: "!unused:example.org",
|
|
||||||
};
|
};
|
||||||
const RealDate = global.Date;
|
|
||||||
class MockDate extends Date {
|
|
||||||
constructor(date: string | number | Date) {
|
|
||||||
super(date || now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockClient = getMockClientWithEventEmitter({});
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
|
timestampToEvent: jest.fn(),
|
||||||
|
});
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
render(
|
render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
|
@ -55,11 +61,11 @@ describe("DateSeparator", () => {
|
||||||
|
|
||||||
type TestCase = [string, number, string];
|
type TestCase = [string, number, string];
|
||||||
const testCases: TestCase[] = [
|
const testCases: TestCase[] = [
|
||||||
["the exact same moment", nowMs, "Today"],
|
["the exact same moment", nowDate.getTime(), "Today"],
|
||||||
["same day as current day", nowMs - HOUR_MS, "Today"],
|
["same day as current day", nowDate.getTime() - HOUR_MS, "Today"],
|
||||||
["day before the current day", nowMs - HOUR_MS * 12, "Yesterday"],
|
["day before the current day", nowDate.getTime() - HOUR_MS * 12, "Yesterday"],
|
||||||
["2 days ago", nowMs - DAY_MS * 2, "Wednesday"],
|
["2 days ago", nowDate.getTime() - DAY_MS * 2, "Wednesday"],
|
||||||
["144 hours ago", nowMs - HOUR_MS * 144, "Sat, Dec 11 2021"],
|
["144 hours ago", nowDate.getTime() - HOUR_MS * 144, "Sat, Dec 11 2021"],
|
||||||
[
|
[
|
||||||
"6 days ago, but less than 144h",
|
"6 days ago, but less than 144h",
|
||||||
new Date("Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)").getTime(),
|
new Date("Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)").getTime(),
|
||||||
|
@ -68,16 +74,20 @@ describe("DateSeparator", () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.Date = MockDate as unknown as DateConstructor;
|
// Set a consistent fake time here so the test is always consistent
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(nowDate.getTime());
|
||||||
|
|
||||||
(SettingsStore.getValue as jest.Mock) = jest.fn((arg) => {
|
(SettingsStore.getValue as jest.Mock) = jest.fn((arg) => {
|
||||||
if (arg === UIFeature.TimelineEnableRelativeDates) {
|
if (arg === UIFeature.TimelineEnableRelativeDates) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
global.Date = RealDate;
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the date separator correctly", () => {
|
it("renders the date separator correctly", () => {
|
||||||
|
@ -115,15 +125,183 @@ describe("DateSeparator", () => {
|
||||||
|
|
||||||
describe("when feature_jump_to_date is enabled", () => {
|
describe("when feature_jump_to_date is enabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
mocked(SettingsStore).getValue.mockImplementation((arg): any => {
|
mocked(SettingsStore).getValue.mockImplementation((arg): any => {
|
||||||
if (arg === "feature_jump_to_date") {
|
if (arg === "feature_jump_to_date") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
jest.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await clearAllModals();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders the date separator correctly", () => {
|
it("renders the date separator correctly", () => {
|
||||||
const { asFragment } = getComponent();
|
const { asFragment } = getComponent();
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
timeDescriptor: "last week",
|
||||||
|
jumpButtonTestId: "jump-to-date-last-week",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeDescriptor: "last month",
|
||||||
|
jumpButtonTestId: "jump-to-date-last-month",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeDescriptor: "the beginning",
|
||||||
|
jumpButtonTestId: "jump-to-date-beginning",
|
||||||
|
},
|
||||||
|
].forEach((testCase) => {
|
||||||
|
it(`can jump to ${testCase.timeDescriptor}`, async () => {
|
||||||
|
// Render the component
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
// Open the jump to date context menu
|
||||||
|
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||||
|
|
||||||
|
// Jump to "x"
|
||||||
|
const returnedDate = new Date();
|
||||||
|
// Just an arbitrary date before "now"
|
||||||
|
returnedDate.setDate(nowDate.getDate() - 100);
|
||||||
|
const returnedEventId = "$abc";
|
||||||
|
mockClient.timestampToEvent.mockResolvedValue({
|
||||||
|
event_id: returnedEventId,
|
||||||
|
origin_server_ts: String(returnedDate.getTime()),
|
||||||
|
} satisfies TimestampToEventResponse);
|
||||||
|
const jumpToXButton = await screen.findByTestId(testCase.jumpButtonTestId);
|
||||||
|
fireEvent.click(jumpToXButton);
|
||||||
|
|
||||||
|
// Flush out the dispatcher which uses `window.setTimeout(...)` since we're
|
||||||
|
// using `jest.useFakeTimers()` in these tests.
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
// Ensure that we're jumping to the event at the given date
|
||||||
|
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
event_id: returnedEventId,
|
||||||
|
highlighted: true,
|
||||||
|
room_id: roomId,
|
||||||
|
metricsTrigger: undefined,
|
||||||
|
} satisfies ViewRoomPayload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not jump to date if we already switched to another room", async () => {
|
||||||
|
// Render the component
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
// Open the jump to date context menu
|
||||||
|
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||||
|
|
||||||
|
// Mimic the outcome of switching rooms while waiting for the jump to date
|
||||||
|
// request to finish. Imagine that we started jumping to "last week", the
|
||||||
|
// network request is taking a while, so we got bored, switched rooms; we
|
||||||
|
// shouldn't jump back to the previous room after the network request
|
||||||
|
// happens to finish later.
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!some-other-room");
|
||||||
|
|
||||||
|
// Jump to "last week"
|
||||||
|
mockClient.timestampToEvent.mockResolvedValue({
|
||||||
|
event_id: "$abc",
|
||||||
|
origin_server_ts: "0",
|
||||||
|
});
|
||||||
|
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||||
|
fireEvent.click(jumpToLastWeekButton);
|
||||||
|
|
||||||
|
// Flush out the dispatcher which uses `window.setTimeout(...)` since we're
|
||||||
|
// using `jest.useFakeTimers()` in these tests.
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
// We should not see any room switching going on (`Action.ViewRoom`)
|
||||||
|
expect(dispatcher.dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show jump to date error if we already switched to another room", async () => {
|
||||||
|
// Render the component
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
// Open the jump to date context menu
|
||||||
|
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||||
|
|
||||||
|
// Mimic the outcome of switching rooms while waiting for the jump to date
|
||||||
|
// request to finish. Imagine that we started jumping to "last week", the
|
||||||
|
// network request is taking a while, so we got bored, switched rooms; we
|
||||||
|
// shouldn't jump back to the previous room after the network request
|
||||||
|
// happens to finish later.
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!some-other-room");
|
||||||
|
|
||||||
|
// Try to jump to "last week" but we want an error to occur and ensure that
|
||||||
|
// we don't show an error dialog for it since we already switched away to
|
||||||
|
// another room and don't care about the outcome here anymore.
|
||||||
|
mockClient.timestampToEvent.mockRejectedValue(new Error("Fake error in test"));
|
||||||
|
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||||
|
fireEvent.click(jumpToLastWeekButton);
|
||||||
|
|
||||||
|
// Wait the necessary time in order to see if any modal will appear
|
||||||
|
await waitEnoughCyclesForModal({
|
||||||
|
useFakeTimers: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We should not see any error modal dialog
|
||||||
|
//
|
||||||
|
// We have to use `queryBy` so that it can return `null` for something that does not exist.
|
||||||
|
expect(screen.queryByTestId("jump-to-date-error-content")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error dialog with submit debug logs option when non-networking error occurs", async () => {
|
||||||
|
// Render the component
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
// Open the jump to date context menu
|
||||||
|
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||||
|
|
||||||
|
// Try to jump to "last week" but we want a non-network error to occur so it
|
||||||
|
// shows the "Submit debug logs" UI
|
||||||
|
mockClient.timestampToEvent.mockRejectedValue(new Error("Fake error in test"));
|
||||||
|
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||||
|
fireEvent.click(jumpToLastWeekButton);
|
||||||
|
|
||||||
|
// Expect error to be shown. We have to wait for the UI to transition.
|
||||||
|
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
new ConnectionError("Fake connection error in test"),
|
||||||
|
new HTTPError("Fake http error in test", 418),
|
||||||
|
new MatrixError(
|
||||||
|
{ errcode: "M_FAKE_ERROR_CODE", error: "Some fake error occured" },
|
||||||
|
518,
|
||||||
|
"https://fake-url/",
|
||||||
|
),
|
||||||
|
].forEach((fakeError) => {
|
||||||
|
it(`should show error dialog without submit debug logs option when networking error (${fakeError.name}) occurs`, async () => {
|
||||||
|
// Render the component
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
// Open the jump to date context menu
|
||||||
|
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||||
|
|
||||||
|
// Try to jump to "last week" but we want a network error to occur
|
||||||
|
mockClient.timestampToEvent.mockRejectedValue(fakeError);
|
||||||
|
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||||
|
fireEvent.click(jumpToLastWeekButton);
|
||||||
|
|
||||||
|
// Expect error to be shown. We have to wait for the UI to transition.
|
||||||
|
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// The submit debug logs option should *NOT* be shown for network errors.
|
||||||
|
//
|
||||||
|
// We have to use `queryBy` so that it can return `null` for something that does not exist.
|
||||||
|
expect(screen.queryByTestId("jump-to-date-error-submit-debug-logs-button")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,6 +44,7 @@ exports[`DateSeparator when feature_jump_to_date is enabled renders the date sep
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-label="Jump to date"
|
aria-label="Jump to date"
|
||||||
class="mx_AccessibleButton mx_DateSeparator_jumpToDateMenu mx_DateSeparator_dateContent"
|
class="mx_AccessibleButton mx_DateSeparator_jumpToDateMenu mx_DateSeparator_dateContent"
|
||||||
|
data-testid="jump-to-date-separator-button"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
|
|
@ -45,7 +45,7 @@ import * as mockVerification from "../../../../src/verification";
|
||||||
import Modal from "../../../../src/Modal";
|
import Modal from "../../../../src/Modal";
|
||||||
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
||||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages";
|
import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages";
|
||||||
import { flushPromises } from "../../../test-utils";
|
import { clearAllModals, flushPromises } from "../../../test-utils";
|
||||||
|
|
||||||
jest.mock("../../../../src/utils/direct-messages", () => ({
|
jest.mock("../../../../src/utils/direct-messages", () => ({
|
||||||
...jest.requireActual("../../../../src/utils/direct-messages"),
|
...jest.requireActual("../../../../src/utils/direct-messages"),
|
||||||
|
@ -417,7 +417,9 @@ describe("<UserOptionsSection />", () => {
|
||||||
mockClient.setIgnoredUsers.mockClear();
|
mockClient.setIgnoredUsers.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => Modal.closeCurrentModal("End of test"));
|
afterEach(async () => {
|
||||||
|
await clearAllModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
inviteSpy.mockRestore();
|
inviteSpy.mockRestore();
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { act, render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
clearAllModals,
|
||||||
createTestClient,
|
createTestClient,
|
||||||
filterConsole,
|
filterConsole,
|
||||||
flushPromises,
|
flushPromises,
|
||||||
|
@ -28,6 +29,7 @@ import {
|
||||||
mkStubRoom,
|
mkStubRoom,
|
||||||
mockPlatformPeg,
|
mockPlatformPeg,
|
||||||
stubClient,
|
stubClient,
|
||||||
|
waitEnoughCyclesForModal,
|
||||||
} from "../../../test-utils";
|
} from "../../../test-utils";
|
||||||
import MessageComposer from "../../../../src/components/views/rooms/MessageComposer";
|
import MessageComposer from "../../../../src/components/views/rooms/MessageComposer";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
@ -48,7 +50,6 @@ import { Action } from "../../../../src/dispatcher/actions";
|
||||||
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
|
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
|
||||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
import Modal from "../../../../src/Modal";
|
|
||||||
|
|
||||||
jest.mock("../../../../src/components/views/rooms/wysiwyg_composer", () => ({
|
jest.mock("../../../../src/components/views/rooms/wysiwyg_composer", () => ({
|
||||||
SendWysiwygComposer: jest.fn().mockImplementation(() => <div data-testid="wysiwyg-composer" />),
|
SendWysiwygComposer: jest.fn().mockImplementation(() => <div data-testid="wysiwyg-composer" />),
|
||||||
|
@ -77,15 +78,9 @@ const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState
|
||||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForModal = async (): Promise<void> => {
|
|
||||||
await flushPromises();
|
|
||||||
await flushPromises();
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldClearModal = async (): Promise<void> => {
|
const shouldClearModal = async (): Promise<void> => {
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
Modal.closeCurrentModal("force");
|
await clearAllModals();
|
||||||
await waitForModal();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -434,7 +429,7 @@ describe("MessageComposer", () => {
|
||||||
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
|
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
|
||||||
wrapAndRender({ room });
|
wrapAndRender({ room });
|
||||||
await startVoiceMessage();
|
await startVoiceMessage();
|
||||||
await waitForModal();
|
await waitEnoughCyclesForModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
shouldClearModal();
|
shouldClearModal();
|
||||||
|
@ -450,7 +445,7 @@ describe("MessageComposer", () => {
|
||||||
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
|
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
|
||||||
wrapAndRender({ room });
|
wrapAndRender({ room });
|
||||||
await startVoiceMessage();
|
await startVoiceMessage();
|
||||||
await waitForModal();
|
await waitEnoughCyclesForModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
shouldClearModal();
|
shouldClearModal();
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
IAuthData,
|
IAuthData,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { clearAllModals } from "../../../../../test-utils";
|
||||||
import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab";
|
import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
import {
|
import {
|
||||||
|
@ -161,7 +162,7 @@ describe("<SessionManagerTab />", () => {
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.spyOn(logger, "error").mockRestore();
|
jest.spyOn(logger, "error").mockRestore();
|
||||||
mockClient.getStoredDevice.mockImplementation((_userId, id) => {
|
mockClient.getStoredDevice.mockImplementation((_userId, id) => {
|
||||||
|
@ -199,7 +200,7 @@ describe("<SessionManagerTab />", () => {
|
||||||
|
|
||||||
// sometimes a verification modal is in modal state when these tests run
|
// sometimes a verification modal is in modal state when these tests run
|
||||||
// make sure the coast is clear
|
// make sure the coast is clear
|
||||||
Modal.closeCurrentModal("");
|
await clearAllModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders spinner while devices load", () => {
|
it("renders spinner while devices load", () => {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import EventEmitter from "events";
|
||||||
import { ActionPayload } from "../../src/dispatcher/payloads";
|
import { ActionPayload } from "../../src/dispatcher/payloads";
|
||||||
import defaultDispatcher from "../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../src/dispatcher/dispatcher";
|
||||||
import { DispatcherAction } from "../../src/dispatcher/actions";
|
import { DispatcherAction } from "../../src/dispatcher/actions";
|
||||||
|
import Modal from "../../src/Modal";
|
||||||
|
|
||||||
export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise((r) => e.once(k, r));
|
export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise((r) => e.once(k, r));
|
||||||
|
|
||||||
|
@ -174,3 +175,42 @@ export const advanceDateAndTime = (ms: number) => {
|
||||||
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
|
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
|
||||||
jest.advanceTimersByTime(ms);
|
jest.advanceTimersByTime(ms);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A horrible hack necessary to wait enough time to ensure any modal is shown after a
|
||||||
|
* `Modal.createDialog(...)` call. We have to contend with the Modal code which renders
|
||||||
|
* things asyncronhously and has weird sleeps which we should strive to remove.
|
||||||
|
*/
|
||||||
|
export const waitEnoughCyclesForModal = async ({
|
||||||
|
useFakeTimers = false,
|
||||||
|
}: {
|
||||||
|
useFakeTimers?: boolean;
|
||||||
|
} = {}): Promise<void> => {
|
||||||
|
// XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead.
|
||||||
|
const flushFunc = useFakeTimers ? flushPromisesWithFakeTimers : flushPromises;
|
||||||
|
|
||||||
|
await flushFunc();
|
||||||
|
await flushFunc();
|
||||||
|
await flushFunc();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A horrible hack necessary to make sure modals don't leak and pollute tests.
|
||||||
|
* `@testing-library/react` automatic cleanup function does not pick up the async modal
|
||||||
|
* rendering and the modals don't unmount when the component unmounts. We should strive
|
||||||
|
* to fix this.
|
||||||
|
*/
|
||||||
|
export const clearAllModals = async (): Promise<void> => {
|
||||||
|
// Prevent modals from leaking and polluting other tests
|
||||||
|
let keepClosingModals = true;
|
||||||
|
while (keepClosingModals) {
|
||||||
|
keepClosingModals = Modal.closeCurrentModal("End of test (clean-up)");
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// of removing the same modal because the promises don't flush otherwise.
|
||||||
|
//
|
||||||
|
// XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead.
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -32,7 +32,13 @@ import {
|
||||||
VoiceBroadcastPlaybackState,
|
VoiceBroadcastPlaybackState,
|
||||||
VoiceBroadcastRecording,
|
VoiceBroadcastRecording,
|
||||||
} from "../../../src/voice-broadcast";
|
} from "../../../src/voice-broadcast";
|
||||||
import { filterConsole, flushPromises, flushPromisesWithFakeTimers, stubClient } from "../../test-utils";
|
import {
|
||||||
|
filterConsole,
|
||||||
|
flushPromises,
|
||||||
|
flushPromisesWithFakeTimers,
|
||||||
|
stubClient,
|
||||||
|
waitEnoughCyclesForModal,
|
||||||
|
} from "../../test-utils";
|
||||||
import { createTestPlayback } from "../../test-utils/audio";
|
import { createTestPlayback } from "../../test-utils/audio";
|
||||||
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||||
import { LazyValue } from "../../../src/utils/LazyValue";
|
import { LazyValue } from "../../../src/utils/LazyValue";
|
||||||
|
@ -77,11 +83,6 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForDialog = async () => {
|
|
||||||
await flushPromises();
|
|
||||||
await flushPromises();
|
|
||||||
};
|
|
||||||
|
|
||||||
const itShouldSetTheStateTo = (state: VoiceBroadcastPlaybackState) => {
|
const itShouldSetTheStateTo = (state: VoiceBroadcastPlaybackState) => {
|
||||||
it(`should set the state to ${state}`, () => {
|
it(`should set the state to ${state}`, () => {
|
||||||
expect(playback.getState()).toBe(state);
|
expect(playback.getState()).toBe(state);
|
||||||
|
@ -480,7 +481,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
jest.spyOn(recording, "stop");
|
jest.spyOn(recording, "stop");
|
||||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
||||||
playback.start();
|
playback.start();
|
||||||
await waitForDialog();
|
await waitEnoughCyclesForModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display a confirm modal", () => {
|
it("should display a confirm modal", () => {
|
||||||
|
|
Loading…
Reference in a new issue