diff --git a/.eslintrc.js b/.eslintrc.js index 4bec4e8320..327119f045 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,14 @@ module.exports = { ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], "Use Media helper instead to centralise access for customisation.", ), + ...buildRestrictedPropertiesOptions(["window.setImmediate"], "Use setTimeout instead."), + ], + "no-restricted-globals": [ + "error", + { + name: "setImmediate", + message: "Use setTimeout instead.", + }, ], "import/no-duplicates": ["error"], diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 90f320409f..f851ddddf6 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -920,7 +920,7 @@ export function logout(oidcClientStore?: OidcClientStore): void { // logout doesn't work for guest sessions // Also we sometimes want to re-log in a guest session if we abort the login. // defer until next tick because it calls a synchronous dispatch, and we are likely here from a dispatch. - setImmediate(() => onLoggedOut()); + setTimeout(onLoggedOut, 0); return; } diff --git a/src/Modal.tsx b/src/Modal.tsx index f39372d532..d93e114a53 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -422,7 +422,7 @@ export class ModalManager extends TypedEventEmitter ); - setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer())); + setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0); } else { // This is safe to call repeatedly if we happen to do that ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 0b71c8dd30..bb19707ce6 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -170,7 +170,7 @@ export default class ContextMenu extends React.PureComponent { + setTimeout(() => { const clickEvent = new MouseEvent("contextmenu", { clientX: x, clientY: y, @@ -180,7 +180,7 @@ export default class ContextMenu extends React.PureComponent { this.mouseHeld = false; // Delaying this to the next event loop tick is necessary for click // event cancellation to work - setImmediate(() => (this.moving = false)); + setTimeout(() => (this.moving = false)); this.snap(true); }; diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index 0064e2541e..e387545b18 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -126,7 +126,7 @@ const BulkRedactDialog: React.FC = (props) => { primaryButtonClass="danger" primaryDisabled={count === 0} onPrimaryButtonClick={() => { - setImmediate(redact); + setTimeout(redact, 0); onFinished(true); }} onCancel={() => onFinished(false)} diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 7ff95edce3..28de13381e 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -354,7 +354,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr placeholder={_t("forward|filter_placeholder")} onSearch={(query: string): void => { setQuery(query); - setImmediate(() => { + setTimeout(() => { const ref = context.state.refs[0]; if (ref) { context.dispatch({ diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 2ac7681afa..2630c85872 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -505,7 +505,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n _setQuery(newQuery); }; useEffect(() => { - setImmediate(() => { + setTimeout(() => { const ref = rovingContext.state.refs[0]; if (ref) { rovingContext.dispatch({ diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 70ea6c23a2..03ea25664d 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -126,7 +126,7 @@ const NewRoomIntro: React.FC = () => { true, ); // focus the topic field to help the user find it as it'll gain an outline - setImmediate(() => { + setTimeout(() => { window.document.getElementById("profileTopic")?.focus(); }); }; diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 6fbccaff6a..7a25f65959 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -289,7 +289,7 @@ export default class RoomSublist extends React.Component { if (payload.action === Action.ViewRoom && payload.show_room_tile && this.state.rooms) { // XXX: we have to do this a tick later because we have incorrect intermediate props during a room change // where we lose the room we are changing from temporarily and then it comes back in an update right after. - setImmediate(() => { + setTimeout(() => { const roomIndex = this.state.rooms.findIndex((r) => r.roomId === payload.room_id); if (!this.state.isExpanded && roomIndex > -1) { @@ -300,7 +300,7 @@ export default class RoomSublist extends React.Component { this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT); this.forceUpdate(); // because the layout doesn't trigger a re-render } - }); + }, 0); } }; @@ -457,9 +457,9 @@ export default class RoomSublist extends React.Component { this.toggleCollapsed(); // if the bottom list is collapsed then scroll it in so it doesn't expand off screen if (!isExpanded && isStickyBottom) { - setImmediate(() => { + setTimeout(() => { sublist.scrollIntoView({ behavior: "smooth" }); - }); + }, 0); } } }; diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index aae949858e..71ff1ef296 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -195,7 +195,7 @@ export class RoomTile extends React.PureComponent { payload.room_id === this.props.room.roomId && payload.show_room_tile ) { - setImmediate(() => { + setTimeout(() => { this.scrollIntoView(); }); } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 16c4d3be5b..7dcb28c325 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -183,7 +183,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements // We do this to intentionally break out of the current event loop task, allowing // us to instead wait for a more convenient time to run our updates. - setImmediate(() => this.onDispatchAsync(payload)); + setTimeout(() => this.onDispatchAsync(payload)); } protected async onDispatchAsync(payload: ActionPayload): Promise { diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index b0318033d6..f24828613e 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -62,7 +62,7 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi this.emit(FILTER_CHANGED); // XXX: Room List Store has a bug where updates to the pre-filter during a local echo of a // tags transition seem to be ignored, so refire in the next tick to work around it - setImmediate(() => { + setTimeout(() => { this.emit(FILTER_CHANGED); }); } diff --git a/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx b/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx index 82255b764f..d2fc8b95a6 100644 --- a/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx +++ b/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx @@ -20,6 +20,7 @@ import { ReceiptType, MatrixClient, PendingEventOrdering, Room } from "matrix-js import { KnownMembership } from "matrix-js-sdk/src/types"; import React from "react"; import userEvent from "@testing-library/user-event"; +import { sleep } from "matrix-js-sdk/src/utils"; import { ChevronFace } from "../../../../src/components/structures/ContextMenu"; import { @@ -141,7 +142,7 @@ describe("RoomGeneralContextMenu", () => { const markAsReadBtn = getByLabelText(container, "Mark as read"); fireEvent.click(markAsReadBtn); - await new Promise(setImmediate); + await sleep(0); expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(event, ReceiptType.Read, true); expect(onFinished).toHaveBeenCalled(); @@ -155,7 +156,7 @@ describe("RoomGeneralContextMenu", () => { const markAsUnreadBtn = getByLabelText(container, "Mark as unread"); fireEvent.click(markAsUnreadBtn); - await new Promise(setImmediate); + await sleep(0); expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", { unread: true, diff --git a/test/components/views/dialogs/ForwardDialog-test.tsx b/test/components/views/dialogs/ForwardDialog-test.tsx index 12c6048e61..c28d19ae6c 100644 --- a/test/components/views/dialogs/ForwardDialog-test.tsx +++ b/test/components/views/dialogs/ForwardDialog-test.tsx @@ -26,6 +26,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { act, fireEvent, getByTestId, render, RenderResult, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { sleep } from "matrix-js-sdk/src/utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog"; @@ -199,7 +200,7 @@ describe("ForwardDialog", () => { await act(async () => { cancelSend(); // Wait one tick for the button to realize the send failed - await new Promise((resolve) => setImmediate(resolve)); + await sleep(0); }); update(); expect(firstButton.className).toContain("mx_ForwardList_sendFailed"); @@ -215,7 +216,7 @@ describe("ForwardDialog", () => { await act(async () => { finishSend(); // Wait one tick for the button to realize the send succeeded - await new Promise((resolve) => setImmediate(resolve)); + await sleep(0); }); update(); expect(secondButton.className).toContain("mx_ForwardList_sent"); diff --git a/test/components/views/right_panel/PinnedMessagesCard-test.tsx b/test/components/views/right_panel/PinnedMessagesCard-test.tsx index aa49c1a55e..d773b51fb9 100644 --- a/test/components/views/right_panel/PinnedMessagesCard-test.tsx +++ b/test/components/views/right_panel/PinnedMessagesCard-test.tsx @@ -32,6 +32,7 @@ import { import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent"; import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent"; +import { sleep } from "matrix-js-sdk/src/utils"; import { stubClient, mkEvent, mkMessage, flushPromises } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -98,7 +99,7 @@ describe("", () => { , ); // Wait a tick for state updates - await new Promise((resolve) => setImmediate(resolve)); + await sleep(0); }); return pins; @@ -114,7 +115,7 @@ describe("", () => { // @ts-ignore what is going on here? pinListener(room.currentState.getStateEvents()); // Wait a tick for state updates - await new Promise((resolve) => setImmediate(resolve)); + await sleep(0); }); }; diff --git a/test/components/views/rooms/RoomPreviewBar-test.tsx b/test/components/views/rooms/RoomPreviewBar-test.tsx index 4bc442fbaa..25758a096b 100644 --- a/test/components/views/rooms/RoomPreviewBar-test.tsx +++ b/test/components/views/rooms/RoomPreviewBar-test.tsx @@ -18,6 +18,7 @@ import React, { ComponentProps } from "react"; import { render, fireEvent, RenderResult, waitFor } from "@testing-library/react"; import { Room, RoomMember, MatrixError, IContent } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; +import { sleep } from "matrix-js-sdk/src/utils"; import { withClientContextRenderOptions, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -373,7 +374,7 @@ describe("", () => { const onJoinClick = jest.fn(); const onRejectClick = jest.fn(); const component = getComponent({ ...props, onJoinClick, onRejectClick }); - await new Promise(setImmediate); + await sleep(0); expect(getPrimaryActionButton(component)).toBeTruthy(); if (expectSecondaryButton) expect(getSecondaryActionButton(component)).toBeFalsy(); fireEvent.click(getPrimaryActionButton(component)!); @@ -387,7 +388,7 @@ describe("", () => { it("renders error message", async () => { const component = getComponent({ inviterName, invitedEmail }); - await new Promise(setImmediate); + await sleep(0); expect(getMessage(component)).toMatchSnapshot(); }); @@ -404,7 +405,7 @@ describe("", () => { it("renders invite message with invited email", async () => { const component = getComponent({ inviterName, invitedEmail }); - await new Promise(setImmediate); + await sleep(0); expect(getMessage(component)).toMatchSnapshot(); }); @@ -420,7 +421,7 @@ describe("", () => { it("renders invite message with invited email", async () => { const component = getComponent({ inviterName, invitedEmail }); - await new Promise(setImmediate); + await sleep(0); expect(getMessage(component)).toMatchSnapshot(); }); @@ -438,7 +439,7 @@ describe("", () => { it("renders email mismatch message when invite email mxid doesnt match", async () => { MatrixClientPeg.safeGet().lookupThreePid = jest.fn().mockReturnValue({ mxid: "not userid" }); const component = getComponent({ inviterName, invitedEmail }); - await new Promise(setImmediate); + await sleep(0); expect(getMessage(component)).toMatchSnapshot(); expect(MatrixClientPeg.safeGet().lookupThreePid).toHaveBeenCalledWith( @@ -452,7 +453,7 @@ describe("", () => { it("renders invite message when invite email mxid match", async () => { MatrixClientPeg.safeGet().lookupThreePid = jest.fn().mockReturnValue({ mxid: userId }); const component = getComponent({ inviterName, invitedEmail }); - await new Promise(setImmediate); + await sleep(0); expect(getMessage(component)).toMatchSnapshot(); await testJoinButton({ inviterName, invitedEmail }, false)(); diff --git a/test/setup/setupManualMocks.ts b/test/setup/setupManualMocks.ts index 0405aa60d6..7b35e721f0 100644 --- a/test/setup/setupManualMocks.ts +++ b/test/setup/setupManualMocks.ts @@ -18,11 +18,6 @@ import fetchMock from "fetch-mock-jest"; import { TextDecoder, TextEncoder } from "util"; import { Response } from "node-fetch"; -// jest 27 removes setImmediate from jsdom -// polyfill until setImmediate use in client can be removed -// @ts-ignore - we know the contract is wrong. That's why we're stubbing it. -global.setImmediate = (callback) => window.setTimeout(callback, 0); - // Stub ResizeObserver // @ts-ignore - we know it's a duplicate (that's why we're stubbing it) class ResizeObserver { diff --git a/test/stores/LifecycleStore-test.ts b/test/stores/LifecycleStore-test.ts index e9801980d9..ae312e11df 100644 --- a/test/stores/LifecycleStore-test.ts +++ b/test/stores/LifecycleStore-test.ts @@ -16,6 +16,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { SyncState } from "matrix-js-sdk/src/matrix"; +import { sleep } from "matrix-js-sdk/src/utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import ToastStore from "../../src/stores/ToastStore"; @@ -40,7 +41,7 @@ describe("LifecycleStore", () => { prevState: SyncState.Prepared, }); - await new Promise(setImmediate); + await sleep(0); expect(addOrReplaceToast).not.toHaveBeenCalledWith( expect.objectContaining({ @@ -58,7 +59,7 @@ describe("LifecycleStore", () => { prevState: SyncState.Prepared, }); - await new Promise(setImmediate); + await sleep(0); expect(addOrReplaceToast).toHaveBeenCalledWith( expect.objectContaining({ @@ -77,7 +78,7 @@ describe("LifecycleStore", () => { prevState: SyncState.Prepared, }); - await new Promise(setImmediate); + await sleep(0); addOrReplaceToast.mock.calls[0][0].props.onAccept(); diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index 55ab305c6b..d70d307e17 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -323,7 +323,6 @@ describe("RoomListStore", () => { const algorithmSpy = jest.spyOn(store.algorithm, "handleRoomUpdate").mockReturnValue(undefined); // @ts-ignore cheat and call protected fn store.onAction({ action: "MatrixActions.accountData", event, previousEvent }); - // flush setImmediate await flushPromises(); expect(algorithmSpy).toHaveBeenCalledWith(normalRoom, RoomUpdateCause.PossibleMuteChange); @@ -346,7 +345,6 @@ describe("RoomListStore", () => { // @ts-ignore cheat and call protected fn store.onAction({ action: "MatrixActions.accountData", event, previousEvent }); - // flush setImmediate await flushPromises(); // only one call to update made for normalRoom diff --git a/test/utils/leave-behaviour-test.ts b/test/utils/leave-behaviour-test.ts index 48117caf48..e5c9f820d9 100644 --- a/test/utils/leave-behaviour-test.ts +++ b/test/utils/leave-behaviour-test.ts @@ -16,6 +16,7 @@ limitations under the License. import { mocked, Mocked } from "jest-mock"; import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { sleep } from "matrix-js-sdk/src/utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "../test-utils"; @@ -78,7 +79,7 @@ describe("leaveRoomBehaviour", () => { const expectDispatch = async (payload: T) => { const dispatcherSpy = jest.fn(); const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - await new Promise((resolve) => setImmediate(resolve)); // Flush the dispatcher + await sleep(0); expect(dispatcherSpy).toHaveBeenCalledWith(payload); defaultDispatcher.unregister(dispatcherRef); };