diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index f9c9ced..6404904 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -686,25 +686,31 @@ export class ClientWidgetApi extends EventEmitter { }); } + let updateDelayedEvent: (delayId: string) => Promise; switch (request.data.action) { case UpdateDelayedEventAction.Cancel: + updateDelayedEvent = this.driver.cancelScheduledDelayedEvent; + break; case UpdateDelayedEventAction.Restart: + updateDelayedEvent = this.driver.restartScheduledDelayedEvent; + break; case UpdateDelayedEventAction.Send: - this.driver - .updateDelayedEvent(request.data.delay_id, request.data.action) - .then(() => { - return this.transport.reply(request, {}); - }) - .catch((e: unknown) => { - console.error("error updating delayed event: ", e); - this.handleDriverError(e, request, "Error updating delayed event"); - }); + updateDelayedEvent = this.driver.sendScheduledDelayedEvent; break; default: return this.transport.reply(request, { error: { message: "Invalid request - unsupported action" }, }); } + updateDelayedEvent + .call(this.driver, request.data.delay_id) + .then(() => { + return this.transport.reply(request, {}); + }) + .catch((e: unknown) => { + console.error("error updating delayed event: ", e); + this.handleDriverError(e, request, "Error updating delayed event"); + }); } private async handleSendToDevice(request: ISendToDeviceFromWidgetActionRequest): Promise { diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 44f0de9..b35f826 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -476,15 +476,38 @@ export class WidgetApi extends EventEmitter { /** * @deprecated This currently relies on an unstable MSC (MSC4157). */ - public updateDelayedEvent( - delayId: string, - action: UpdateDelayedEventAction, - ): Promise { + public cancelScheduledDelayedEvent(delayId: string): Promise { return this.transport.send( WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, { delay_id: delayId, - action, + action: UpdateDelayedEventAction.Cancel, + }, + ); + } + + /** + * @deprecated This currently relies on an unstable MSC (MSC4157). + */ + public restartScheduledDelayedEvent(delayId: string): Promise { + return this.transport.send( + WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + { + delay_id: delayId, + action: UpdateDelayedEventAction.Restart, + }, + ); + } + + /** + * @deprecated This currently relies on an unstable MSC (MSC4157). + */ + public sendScheduledDelayedEvent(delayId: string): Promise { + return this.transport.send( + WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + { + delay_id: delayId, + action: UpdateDelayedEventAction.Send, }, ); } diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index 9663e34..f14a5ed 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -23,7 +23,6 @@ import { IRoomAccountData, ITurnServer, IWidgetApiErrorResponseDataDetails, - UpdateDelayedEventAction, } from ".."; export interface ISendEventDetails { @@ -142,10 +141,32 @@ export abstract class WidgetDriver { /** * @experimental Part of MSC4140 & MSC4157 - * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. - * @throws Rejected when there is no matching delayed event, or when the action failed to run. + * Cancel the scheduled delivery of the delayed event matching the provided {@link delayId}. + * @throws Rejected when there is no matching delayed event, + * or when the delayed event failed to be cancelled. */ - public updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise { + public cancelScheduledDelayedEvent(delayId: string): Promise { + return Promise.reject(new Error("Failed to override function")); + } + + /** + * @experimental Part of MSC4140 & MSC4157 + * Restart the scheduled delivery of the delayed event matching the provided {@link delayId}. + * @throws Rejected when there is no matching delayed event, + * or when the delayed event failed to be restarted. + */ + public restartScheduledDelayedEvent(delayId: string): Promise { + return Promise.reject(new Error("Failed to override function")); + } + + /** + * @experimental Part of MSC4140 & MSC4157 + * Immediately send the delayed event matching the provided {@link delayId}, + * instead of waiting for its scheduled delivery. + * @throws Rejected when there is no matching delayed event, + * or when the delayed event failed to be sent. + */ + public sendScheduledDelayedEvent(delayId: string): Promise { return Promise.reject(new Error("Failed to override function")); } diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 2ac92ec..1a49390 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -128,7 +128,9 @@ describe("ClientWidgetApi", () => { readEventRelations: jest.fn(), sendEvent: jest.fn(), sendDelayedEvent: jest.fn(), - updateDelayedEvent: jest.fn(), + cancelScheduledDelayedEvent: jest.fn(), + restartScheduledDelayedEvent: jest.fn(), + sendScheduledDelayedEvent: jest.fn(), sendToDevice: jest.fn(), askOpenID: jest.fn(), readRoomAccountData: jest.fn(), @@ -903,7 +905,57 @@ describe("ClientWidgetApi", () => { }); describe("update_delayed_event action", () => { - it("fails to update delayed events", async () => { + it("fails to cancel delayed events", async () => { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Cancel, + }, + }; + + await loadIframe([]); // Without the required capability + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }); + }); + + expect(driver.cancelScheduledDelayedEvent).not.toBeCalled(); + }); + + it("fails to restart delayed events", async () => { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Restart, + }, + }; + + await loadIframe([]); // Without the required capability + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }); + }); + + expect(driver.restartScheduledDelayedEvent).not.toBeCalled(); + }); + + it("fails to send delayed events", async () => { const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, widgetId: "test", @@ -925,7 +977,7 @@ describe("ClientWidgetApi", () => { }); }); - expect(driver.updateDelayedEvent).not.toBeCalled(); + expect(driver.sendScheduledDelayedEvent).not.toBeCalled(); }); it("fails to update delayed events with unsupported action", async () => { @@ -950,42 +1002,88 @@ describe("ClientWidgetApi", () => { }); }); - expect(driver.updateDelayedEvent).not.toBeCalled(); + expect(driver.cancelScheduledDelayedEvent).not.toBeCalled(); + expect(driver.restartScheduledDelayedEvent).not.toBeCalled(); + expect(driver.sendScheduledDelayedEvent).not.toBeCalled(); }); - it("updates delayed events", async () => { - driver.updateDelayedEvent.mockResolvedValue(undefined); + it("can cancel delayed events", async () => { + driver.cancelScheduledDelayedEvent.mockResolvedValue(undefined); - for (const action of [ - UpdateDelayedEventAction.Cancel, - UpdateDelayedEventAction.Restart, - UpdateDelayedEventAction.Send, - ]) { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action, - }, - }; + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Cancel, + }, + }; - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); + + expect(driver.cancelScheduledDelayedEvent).toHaveBeenCalledWith(event.data.delay_id); + }); + + it("can restart delayed events", async () => { + driver.restartScheduledDelayedEvent.mockResolvedValue(undefined); + + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Restart, + }, + }; + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); + + expect(driver.restartScheduledDelayedEvent).toHaveBeenCalledWith(event.data.delay_id); + }); + + it("can send delayed events", async () => { + driver.sendScheduledDelayedEvent.mockResolvedValue(undefined); + + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + }; + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); - expect(driver.updateDelayedEvent).toHaveBeenCalledWith(event.data.delay_id, event.data.action); - } + expect(driver.sendScheduledDelayedEvent).toHaveBeenCalledWith(event.data.delay_id); }); it("should reject requests when the driver throws an exception", async () => { - driver.updateDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); + driver.sendScheduledDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1012,7 +1110,7 @@ describe("ClientWidgetApi", () => { it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); - driver.updateDelayedEvent.mockRejectedValue( + driver.sendScheduledDelayedEvent.mockRejectedValue( new CustomMatrixError("failed to update delayed event", 400, "M_NOT_JSON", { reason: "Content must be a JSON object.", }), diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index b128e1c..89273a9 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -32,7 +32,6 @@ import { IWidgetApiRequestData, IWidgetApiResponse, IWidgetApiResponseData, - UpdateDelayedEventAction, WidgetApiDirection, } from "../src"; @@ -394,43 +393,59 @@ describe("WidgetApi", () => { describe("updateDelayedEvent", () => { it("updates delayed events", async () => { - widgetTransportHelper.queueResponse({}); - await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).resolves.toEqual({}); + for (const updateDelayedEvent of [ + widgetApi.cancelScheduledDelayedEvent, + widgetApi.restartScheduledDelayedEvent, + widgetApi.sendScheduledDelayedEvent, + ]) { + widgetTransportHelper.queueResponse({}); + await expect(updateDelayedEvent.call(widgetApi, "id")).resolves.toEqual({}); + } }); it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( - "An error occurred", - ); + for (const updateDelayedEvent of [ + widgetApi.cancelScheduledDelayedEvent, + widgetApi.restartScheduledDelayedEvent, + widgetApi.sendScheduledDelayedEvent, + ]) { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData); + + await expect(updateDelayedEvent.call(widgetApi, "id")).rejects.toThrow("An error occurred"); + } }); it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", + for (const updateDelayedEvent of [ + widgetApi.cancelScheduledDelayedEvent, + widgetApi.restartScheduledDelayedEvent, + widgetApi.sendScheduledDelayedEvent, + ]) { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, }, - }, - }; + }; - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); + await expect(updateDelayedEvent.call(widgetApi, "id")).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + } }); });