diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index b99b69c40e0..bab22127aaa 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -481,7 +481,7 @@ export class StopGapWidgetDriver extends WidgetDriver { if (room === null) return []; const results: MatrixEvent[] = []; const events = room.getLiveTimeline().getEvents(); // timelines are most recent last - for (let i = events.length - 1; i > 0; i--) { + for (let i = events.length - 1; i >= 0; i--) { const ev = events[i]; if (results.length >= limit) break; if (since !== undefined && ev.getId() === since) break; diff --git a/test/unit-tests/stores/widgets/StopGapWidget-test.ts b/test/unit-tests/stores/widgets/StopGapWidget-test.ts index 493ac3b8a40..61e96886b90 100644 --- a/test/unit-tests/stores/widgets/StopGapWidget-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidget-test.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { mocked, MockedObject } from "jest-mock"; +import { mocked, MockedFunction, MockedObject } from "jest-mock"; import { last } from "lodash"; import { MatrixEvent, @@ -15,15 +15,20 @@ import { EventTimeline, EventType, MatrixEventEvent, + RoomStateEvent, + RoomState, } from "matrix-js-sdk/src/matrix"; import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api"; import { waitFor } from "jest-matrix-react"; +import { Optional } from "matrix-events-sdk"; import { stubClient, mkRoom, mkEvent } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget"; import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore"; import SettingsStore from "../../../../src/settings/SettingsStore"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; +import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore"; jest.mock("matrix-widget-api/lib/ClientWidgetApi"); @@ -53,6 +58,7 @@ describe("StopGapWidget", () => { // Start messaging without an iframe, since ClientWidgetApi is mocked widget.startMessaging(null as unknown as HTMLIFrameElement); messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!); + messaging.feedStateUpdate.mockResolvedValue(); }); afterEach(() => { @@ -84,6 +90,20 @@ describe("StopGapWidget", () => { expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false); }); + it("feeds incoming state updates to the widget", () => { + const event = mkEvent({ + event: true, + type: "org.example.foo", + skey: "", + user: "@alice:example.org", + content: { hello: "world" }, + room: "!1:example.org", + }); + + client.emit(RoomStateEvent.Events, event, {} as unknown as RoomState, null); + expect(messaging.feedStateUpdate).toHaveBeenCalledWith(event.getEffectiveEvent()); + }); + describe("feed event", () => { let event1: MatrixEvent; let event2: MatrixEvent; @@ -223,6 +243,7 @@ describe("StopGapWidget", () => { }); }); }); + describe("StopGapWidget with stickyPromise", () => { let client: MockedObject; let widget: StopGapWidget; @@ -288,3 +309,49 @@ describe("StopGapWidget with stickyPromise", () => { waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 }); }); }); + +describe("StopGapWidget as an account widget", () => { + let widget: StopGapWidget; + let messaging: MockedObject; + let getRoomId: MockedFunction<() => Optional>; + + beforeEach(() => { + stubClient(); + // I give up, getting the return type of spyOn right is hopeless + getRoomId = jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId") as unknown as MockedFunction< + () => Optional + >; + getRoomId.mockReturnValue("!1:example.org"); + + widget = new StopGapWidget({ + app: { + id: "test", + creatorUserId: "@alice:example.org", + type: "example", + url: "https://example.org?user-id=$matrix_user_id&device-id=$org.matrix.msc3819.matrix_device_id&base-url=$org.matrix.msc4039.matrix_base_url&theme=$org.matrix.msc2873.client_theme", + roomId: "!1:example.org", + }, + userId: "@alice:example.org", + creatorUserId: "@alice:example.org", + waitForIframeLoad: true, + userWidget: false, + }); + // Start messaging without an iframe, since ClientWidgetApi is mocked + widget.startMessaging(null as unknown as HTMLIFrameElement); + messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!); + }); + + afterEach(() => { + widget.stopMessaging(); + getRoomId.mockRestore(); + }); + + it("updates viewed room", () => { + expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(1); + expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!1:example.org"); + getRoomId.mockReturnValue("!2:example.org"); + SdkContextClass.instance.roomViewStore.emit(UPDATE_EVENT); + expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(2); + expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!2:example.org"); + }); +}); diff --git a/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts b/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts index e82743e8dd8..ccf2638d506 100644 --- a/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts @@ -17,6 +17,7 @@ import { MatrixEvent, MsgType, RelationType, + Room, } from "matrix-js-sdk/src/matrix"; import { Widget, @@ -38,7 +39,7 @@ import { import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver"; -import { stubClient } from "../../../test-utils"; +import { mkEvent, stubClient } from "../../../test-utils"; import { ModuleRunner } from "../../../../src/modules/ModuleRunner"; import dis from "../../../../src/dispatcher/dispatcher"; import Modal from "../../../../src/Modal"; @@ -692,4 +693,107 @@ describe("StopGapWidgetDriver", () => { await expect(file.text()).resolves.toEqual("test contents"); }); }); + + describe("readRoomTimeline", () => { + const event1 = mkEvent({ + event: true, + id: "$event-id1", + type: "org.example.foo", + user: "@alice:example.org", + content: { hello: "world" }, + room: "!1:example.org", + }); + const event2 = mkEvent({ + event: true, + id: "$event-id2", + type: "org.example.foo", + user: "@alice:example.org", + content: { hello: "world" }, + room: "!1:example.org", + }); + let driver: WidgetDriver; + + beforeEach(() => { + driver = mkDefaultDriver(); + client.getRoom.mockReturnValue({ + getLiveTimeline: () => ({ getEvents: () => [event1, event2] }), + } as unknown as Room); + }); + + it("reads all events", async () => { + expect( + await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 10, undefined), + ).toEqual([event2, event1].map((e) => e.getEffectiveEvent())); + }); + + it("reads up to a limit", async () => { + expect( + await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 1, undefined), + ).toEqual([event2.getEffectiveEvent()]); + }); + + it("reads up to a specific event", async () => { + expect( + await driver.readRoomTimeline( + "!1:example.org", + "org.example.foo", + undefined, + undefined, + 10, + event1.getId(), + ), + ).toEqual([event2.getEffectiveEvent()]); + }); + }); + + describe("readRoomState", () => { + const event1 = mkEvent({ + event: true, + id: "$event-id1", + type: "org.example.foo", + user: "@alice:example.org", + content: { hello: "world" }, + skey: "1", + room: "!1:example.org", + }); + const event2 = mkEvent({ + event: true, + id: "$event-id2", + type: "org.example.foo", + user: "@alice:example.org", + content: { hello: "world" }, + skey: "2", + room: "!1:example.org", + }); + let driver: WidgetDriver; + let getStateEvents: jest.Mock; + + beforeEach(() => { + driver = mkDefaultDriver(); + getStateEvents = jest.fn(); + client.getRoom.mockReturnValue({ + getLiveTimeline: () => ({ getState: () => ({ getStateEvents }) }), + } as unknown as Room); + }); + + it("reads a specific state key", async () => { + getStateEvents.mockImplementation((eventType, stateKey) => { + if (eventType === "org.example.foo" && stateKey === "1") return event1; + return undefined; + }); + expect(await driver.readRoomState("!1:example.org", "org.example.foo", "1")).toEqual([ + event1.getEffectiveEvent(), + ]); + }); + + it("reads all state keys", async () => { + getStateEvents.mockImplementation((eventType, stateKey) => { + if (eventType === "org.example.foo" && stateKey === undefined) return [event1, event2]; + return []; + }); + expect(await driver.readRoomState("!1:example.org", "org.example.foo", undefined)).toEqual( + [event1, event2].map((e) => e.getEffectiveEvent()), + ); + }); + }); });