diff --git a/src/viewmodels/room/RoomStatusBar.ts b/src/viewmodels/room/RoomStatusBar.ts index 00c9ebb958..3d0c1e1d96 100644 --- a/src/viewmodels/room/RoomStatusBar.ts +++ b/src/viewmodels/room/RoomStatusBar.ts @@ -28,7 +28,7 @@ import dis from "../../dispatcher/dispatcher"; import { LocalRoom, LocalRoomState } from "../../models/LocalRoom"; interface PropsWithRoom { - room: Room; + room: Room | LocalRoom; } interface PropsWithVisibility extends PropsWithRoom { /** @@ -52,6 +52,7 @@ export class RoomStatusBarViewModel hasClickedTermsAndConditions: boolean, ): RoomStatusBarViewSnapshot => { const unsentMessages = room.getPendingEvents().filter((ev) => ev.status === EventStatus.NOT_SENT); + console.log({ unsentMessages }); if (unsentMessages.length === 0) { return { state: null, @@ -98,15 +99,12 @@ export class RoomStatusBarViewModel isResending: boolean, hasClickedTermsAndConditions: boolean, ): RoomStatusBarViewSnapshot => { - if (room instanceof LocalRoom) { - if (room.isError) { - return { - state: RoomStatusBarState.LocalRoomFailed, - }; - } else { - // Local rooms do not have to worry about these other conditions :) - return { state: null }; - } + const isLocalRoomAndIsError = (room as LocalRoom)["isError"]; + if (isLocalRoomAndIsError !== undefined) { + return { + // Local rooms do not have to worry about these other conditions + state: isLocalRoomAndIsError ? RoomStatusBarState.LocalRoomFailed : null, + }; } // If we're in the process of resending, don't flicker. diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index e43665a167..a9db37f5df 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -303,6 +303,7 @@ export function createTestClient(): MatrixClient { room_id: roomId, }); }), + resendEvent: jest.fn().mockResolvedValue({}), _unstable_sendDelayedEvent: jest.fn(), _unstable_sendDelayedStateEvent: jest.fn(), @@ -703,7 +704,7 @@ export function mkStubRoom( getMembersWithMembership: jest.fn().mockReturnValue([]), getMxcAvatarUrl: () => "mxc://avatar.url/room.png", getMyMembership: jest.fn().mockReturnValue(KnownMembership.Join), - getPendingEvents: () => [] as MatrixEvent[], + getPendingEvents: jest.fn().mockReturnValue([]), getReceiptsForEvent: jest.fn().mockReturnValue([]), getRecommendedVersion: jest.fn().mockReturnValue(Promise.resolve("")), getThreads: jest.fn().mockReturnValue([]), diff --git a/test/viewmodels/room/RoomStatusBar-test.ts b/test/viewmodels/room/RoomStatusBar-test.ts new file mode 100644 index 0000000000..0475da14aa --- /dev/null +++ b/test/viewmodels/room/RoomStatusBar-test.ts @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { RoomStatusBarViewModel } from "../../../src/viewmodels/room/RoomStatusBar"; +import { mkEvent, mkRoom, stubClient } from "../../test-utils"; +import { + SyncState, + MatrixError, + ClientEvent, + MatrixClient, + Room, + type MatrixEvent, + EventStatus, +} from "matrix-js-sdk/src/matrix"; +import { RoomStatusBarState } from "@element-hq/web-shared-components"; +import { type MockedObject } from "jest-mock"; +import { LocalRoom, LocalRoomState } from "../../../src/models/LocalRoom"; + +const userId = "@example:example.org"; + +function mkEventWithError(error: MatrixError): MatrixEvent { + const event = mkEvent({ + event: true, + user: userId, + type: "org.example.test", + content: {}, + status: EventStatus.NOT_SENT, + }); + event.error = error; + return event; +} + +describe("RoomStatusBarViewModel", () => { + let client: MockedObject; + let vm: RoomStatusBarViewModel; + let room: MockedObject; + let roomEmitFn!: () => void; + beforeEach(() => { + client = stubClient() as MockedObject; + room = mkRoom(client, "!example"); + room.on.mockImplementationOnce((_event, fn) => { + roomEmitFn = fn as any; + return room; + }); + vm = new RoomStatusBarViewModel({ + room, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should not be visible by default", () => { + expect(vm.getSnapshot()).toEqual({ state: null }); + }); + + it("should resolve state to ConnectionLost on failed sync", () => { + client.getSyncState.mockReturnValue(SyncState.Error); + client.emit(ClientEvent.Sync, SyncState.Error, null); + expect(vm.getSnapshot()).toEqual({ state: RoomStatusBarState.ConnectionLost }); + }); + + // Because we expect LoggedInView to pop a toast + it("should resolve state to nothing if sync error is M_RESOURCE_LIMIT_EXCEEDED ", () => { + client.getSyncState.mockReturnValue(SyncState.Error); + client.getSyncStateData.mockReturnValue({ error: new MatrixError({ errcode: "M_RESOURCE_LIMIT_EXCEEDED" }) }); + client.emit(ClientEvent.Sync, SyncState.Error, null); + expect(vm.getSnapshot()).toEqual({ state: null }); + }); + + it("should resolve state to NeedsConsent if a pending event has a M_CONSENT_NOT_GIVEN error", () => { + room.getPendingEvents.mockReturnValue([ + mkEventWithError(new MatrixError({ errcode: "M_CONSENT_NOT_GIVEN", consent_uri: "https://example.org" })), + ]); + roomEmitFn(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.NeedsConsent, + consentUri: "https://example.org", + }); + }); + + it("should resolve state to UnsentMessages once onTermsAndConditionsClicked is called", () => { + room.getPendingEvents.mockReturnValue([mkEventWithError(new MatrixError({ errcode: "M_CONSENT_NOT_GIVEN" }))]); + roomEmitFn(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.NeedsConsent, + }); + vm.onTermsAndConditionsClicked(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.UnsentMessages, + isResending: false, + }); + }); + + it("should resolve state to ResourceLimited if a pending event has a M_RESOURCE_LIMIT_EXCEEDED error", () => { + room.getPendingEvents.mockReturnValue([ + mkEventWithError( + new MatrixError({ + errcode: "M_RESOURCE_LIMIT_EXCEEDED", + limit_type: "hs_disabled", + admin_contact: "https://example.org", + }), + ), + ]); + roomEmitFn(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.ResourceLimited, + adminContactHref: "https://example.org", + resourceLimit: "hs_disabled", + }); + }); + + it("should resolve state to UnsentMessages if there are any other events", () => { + room.getPendingEvents.mockReturnValue([mkEventWithError(new MatrixError({ errcode: "M_UNKNOWN" }))]); + roomEmitFn(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.UnsentMessages, + isResending: false, + }); + }); + + it("should resolve state to isResending=true once onResendAllClick is called", () => { + room.getPendingEvents.mockReturnValue([mkEventWithError(new MatrixError({ errcode: "M_UNKNOWN" }))]); + roomEmitFn(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.UnsentMessages, + isResending: false, + }); + vm.onResendAllClick(); + expect(vm.getSnapshot()).toEqual({ + state: RoomStatusBarState.UnsentMessages, + isResending: true, + }); + expect(client.resendEvent).toHaveBeenCalledTimes(1); + }); + + describe("Local rooms", () => { + it("should resolve state to LocalRoomFailed if room fails to be created", () => { + const localRoom = new LocalRoom("!example", client, userId); + localRoom.state = LocalRoomState.ERROR; + vm = new RoomStatusBarViewModel({ + room: localRoom, + }); + expect(vm.getSnapshot()).toEqual({ state: RoomStatusBarState.LocalRoomFailed }); + }); + it("should resolve state to nothing for any other state for localroom", () => { + const localRoom = new LocalRoom("!example", client, userId); + localRoom.state = LocalRoomState.NEW; + vm = new RoomStatusBarViewModel({ + room: localRoom, + }); + expect(vm.getSnapshot()).toEqual({ state: null }); + }); + }); +});