From 06d0ff3793a1ebbf83588c1617d047afb73c073b Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Thu, 30 Oct 2025 16:26:44 -0400 Subject: [PATCH 01/23] Apply some code quality suggestions from SonarQube --- src/ClientWidgetApi.ts | 6 +++--- src/WidgetApi.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index f9c9ced..792aecd 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -230,7 +230,7 @@ export class ClientWidgetApi extends EventEmitter { public async getWidgetVersions(): Promise { if (Array.isArray(this.cachedWidgetVersions)) { - return Promise.resolve(this.cachedWidgetVersions); + return this.cachedWidgetVersions; } try { @@ -1141,7 +1141,7 @@ export class ClientWidgetApi extends EventEmitter { private async flushRoomState(): Promise { try { // Only send a single action once all concurrent tasks have completed - do await Promise.all([...this.pushRoomStateTasks]); + do await Promise.all(this.pushRoomStateTasks); while (this.pushRoomStateTasks.size > 0); const events: IRoomEvent[] = []; @@ -1251,7 +1251,7 @@ export class ClientWidgetApi extends EventEmitter { eventTypeMap.set(rawEvent.type, stateKeyMap); } if (!stateKeyMap.has(rawEvent.type)) stateKeyMap.set(rawEvent.state_key, rawEvent); - do await Promise.all([...this.pushRoomStateTasks]); + do await Promise.all(this.pushRoomStateTasks); while (this.pushRoomStateTasks.size > 0); await this.flushRoomStateTask; } diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 44f0de9..b394c5d 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -191,7 +191,9 @@ export class WidgetApi extends EventEmitter { * @throws Throws if the capabilities negotiation has already started. */ public requestCapabilities(capabilities: Capability[]): void { - capabilities.forEach((cap) => this.requestCapability(cap)); + for (const cap of capabilities) { + this.requestCapability(cap); + } } /** From ff5e54ee80dd8e916603979f2262eac5b15e2b03 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 31 Oct 2025 08:36:52 -0400 Subject: [PATCH 02/23] Prefer globalThis over window https://sonarcloud.io/organizations/matrix-org/rules?open=typescript%3AS7764&rule_key=typescript%3AS7764 --- src/ClientWidgetApi.ts | 2 +- src/WidgetApi.ts | 4 ++-- src/transport/PostmessageTransport.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 792aecd..dcdefcb 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -175,7 +175,7 @@ export class ClientWidgetApi extends EventEmitter { if (!driver) { throw new Error("Invalid driver"); } - this.transport = new PostmessageTransport(WidgetApiDirection.ToWidget, widget.id, iframe.contentWindow, window); + this.transport = new PostmessageTransport(WidgetApiDirection.ToWidget, widget.id, iframe.contentWindow, globalThis); this.transport.targetOrigin = widget.origin; this.transport.on("message", this.handleMessage.bind(this)); diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index b394c5d..1d3abbf 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -147,10 +147,10 @@ export class WidgetApi extends EventEmitter { private clientOrigin: string | null = null, ) { super(); - if (!window.parent) { + if (!globalThis.parent) { throw new Error("No parent window. This widget doesn't appear to be embedded properly."); } - this.transport = new PostmessageTransport(WidgetApiDirection.FromWidget, widgetId, window.parent, window); + this.transport = new PostmessageTransport(WidgetApiDirection.FromWidget, widgetId, globalThis.parent, globalThis); this.transport.targetOrigin = clientOrigin; this.transport.on("message", this.handleMessage.bind(this)); } diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index 4589735..faa9289 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -60,8 +60,8 @@ export class PostmessageTransport extends EventEmitter implements ITransport { public constructor( private sendDirection: WidgetApiDirection, private initialWidgetId: string | null, - private transportWindow: Window, - private inboundWindow: Window, + private transportWindow: Window | typeof globalThis, + private inboundWindow: Window | typeof globalThis, ) { super(); this._widgetId = initialWidgetId; From 505138ec39d9bbc2d0a6399c905657ca89a04f0a Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 31 Oct 2025 08:52:23 -0400 Subject: [PATCH 03/23] De-negate delayed event conditions --- src/ClientWidgetApi.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index dcdefcb..c26c88e 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -609,17 +609,17 @@ export class ClientWidgetApi extends EventEmitter { }); } - if (!isDelayedEvent) { - sendEventPromise = this.driver.sendEvent( + if (isDelayedEvent) { + sendEventPromise = this.driver.sendDelayedEvent( + request.data.delay ?? null, + request.data.parent_delay_id ?? null, request.data.type, request.data.content || {}, request.data.state_key, request.data.room_id, ); } else { - sendEventPromise = this.driver.sendDelayedEvent( - request.data.delay ?? null, - request.data.parent_delay_id ?? null, + sendEventPromise = this.driver.sendEvent( request.data.type, request.data.content || {}, request.data.state_key, @@ -635,17 +635,17 @@ export class ClientWidgetApi extends EventEmitter { }); } - if (!isDelayedEvent) { - sendEventPromise = this.driver.sendEvent( + if (isDelayedEvent) { + sendEventPromise = this.driver.sendDelayedEvent( + request.data.delay ?? null, + request.data.parent_delay_id ?? null, request.data.type, content, null, // not sending a state event request.data.room_id, ); } else { - sendEventPromise = this.driver.sendDelayedEvent( - request.data.delay ?? null, - request.data.parent_delay_id ?? null, + sendEventPromise = this.driver.sendEvent( request.data.type, content, null, // not sending a state event From c2ab73bdaa5e6b42e705ac59dbedc65b60bd043d Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 31 Oct 2025 09:11:38 -0400 Subject: [PATCH 04/23] Format with Prettier --- src/ClientWidgetApi.ts | 7 ++++++- src/WidgetApi.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index c26c88e..5ac5a19 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -175,7 +175,12 @@ export class ClientWidgetApi extends EventEmitter { if (!driver) { throw new Error("Invalid driver"); } - this.transport = new PostmessageTransport(WidgetApiDirection.ToWidget, widget.id, iframe.contentWindow, globalThis); + this.transport = new PostmessageTransport( + WidgetApiDirection.ToWidget, + widget.id, + iframe.contentWindow, + globalThis, + ); this.transport.targetOrigin = widget.origin; this.transport.on("message", this.handleMessage.bind(this)); diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 1d3abbf..1a61ddf 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -150,7 +150,12 @@ export class WidgetApi extends EventEmitter { if (!globalThis.parent) { throw new Error("No parent window. This widget doesn't appear to be embedded properly."); } - this.transport = new PostmessageTransport(WidgetApiDirection.FromWidget, widgetId, globalThis.parent, globalThis); + this.transport = new PostmessageTransport( + WidgetApiDirection.FromWidget, + widgetId, + globalThis.parent, + globalThis, + ); this.transport.targetOrigin = clientOrigin; this.transport.on("message", this.handleMessage.bind(this)); } From 7562b89fb52d0c1f341145f7dc31fb155ac3e826 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 31 Oct 2025 09:13:08 -0400 Subject: [PATCH 05/23] More window -> globalThis --- examples/widget/utils.js | 2 +- src/transport/PostmessageTransport.ts | 2 +- test/WidgetApi-test.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/widget/utils.js b/examples/widget/utils.js index 705a6f0..46eb78b 100644 --- a/examples/widget/utils.js +++ b/examples/widget/utils.js @@ -15,7 +15,7 @@ */ function parseFragment() { - const fragmentString = window.location.hash || "?"; + const fragmentString = globalThis.location.hash || "?"; return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0))); } diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index faa9289..df93ca6 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -159,7 +159,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { if (this.stopController.signal.aborted) return; if (!ev.data) return; // invalid event - if (this.strictOriginCheck && ev.origin !== window.origin) return; // bad origin + if (this.strictOriginCheck && ev.origin !== globalThis.origin) return; // bad origin // treat the message as a response first, then downgrade to a request const response = ev.data; diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index b128e1c..3710242 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -100,7 +100,7 @@ describe("WidgetApi", () => { const response = clientTrafficHelper.nextQueuedResponse(); if (response) { - window.postMessage( + globalThis.postMessage( { ...request, response: response, @@ -109,14 +109,14 @@ describe("WidgetApi", () => { ); } }; - window.addEventListener("message", clientListener); + globalThis.addEventListener("message", clientListener); widgetApi = new WidgetApi("WidgetApi-test", "*"); widgetApi.start(); }); afterEach(() => { - window.removeEventListener("message", clientListener); + globalThis.removeEventListener("message", clientListener); }); describe("readEventRelations", () => { From ff478547c86b069858bbf860ff4f98a75b247d9a Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 31 Oct 2025 09:43:24 -0400 Subject: [PATCH 06/23] Apply prefer-readonly lint rule --- .eslintrc.js | 1 + src/ClientWidgetApi.ts | 12 ++++++------ src/WidgetApi.ts | 4 ++-- src/models/Widget.ts | 2 +- src/transport/PostmessageTransport.ts | 12 ++++++------ test/WidgetApi-test.ts | 4 ++-- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 96ca83a..ab91608 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,7 @@ module.exports = { asyncArrow: "always", }, ], + "@typescript-eslint/prefer-readonly": ["error"], "arrow-parens": "off", "prefer-promise-reject-errors": "off", "quotes": "off", diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 5ac5a19..8f36cb0 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -141,15 +141,15 @@ export class ClientWidgetApi extends EventEmitter { private cachedWidgetVersions: ApiVersion[] | null = null; // contentLoadedActionSent is used to check that only one ContentLoaded request is send. private contentLoadedActionSent = false; - private allowedCapabilities = new Set(); - private allowedEvents: WidgetEventCapability[] = []; + private readonly allowedCapabilities = new Set(); + private readonly allowedEvents: WidgetEventCapability[] = []; private isStopped = false; private turnServers: AsyncGenerator | null = null; private contentLoadedWaitTimer?: ReturnType; // Stores pending requests to push a room's state to the widget - private pushRoomStateTasks = new Set>(); + private readonly pushRoomStateTasks = new Set>(); // Room ID → event type → state key → events to be pushed - private pushRoomStateResult = new Map>>(); + private readonly pushRoomStateResult = new Map>>(); private flushRoomStateTask: Promise | null = null; /** @@ -162,8 +162,8 @@ export class ClientWidgetApi extends EventEmitter { */ public constructor( public readonly widget: Widget, - private iframe: HTMLIFrameElement, - private driver: WidgetDriver, + private readonly iframe: HTMLIFrameElement, + private readonly driver: WidgetDriver, ) { super(); if (!iframe?.contentWindow) { diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 1a61ddf..99cf026 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -131,7 +131,7 @@ export class WidgetApi extends EventEmitter { private capabilitiesFinished = false; private supportsMSC2974Renegotiate = false; - private requestedCapabilities: Capability[] = []; + private readonly requestedCapabilities: Capability[] = []; private approvedCapabilities?: Capability[]; private cachedClientVersions?: ApiVersion[]; private turnServerWatchers = 0; @@ -144,7 +144,7 @@ export class WidgetApi extends EventEmitter { */ public constructor( widgetId: string | null = null, - private clientOrigin: string | null = null, + private readonly clientOrigin: string | null = null, ) { super(); if (!globalThis.parent) { diff --git a/src/models/Widget.ts b/src/models/Widget.ts index 0b66452..6d5bde0 100644 --- a/src/models/Widget.ts +++ b/src/models/Widget.ts @@ -22,7 +22,7 @@ import { ITemplateParams, runTemplate } from ".."; * Represents the barest form of widget. */ export class Widget { - public constructor(private definition: IWidget) { + public constructor(private readonly definition: IWidget) { if (!this.definition) throw new Error("Definition is required"); assertPresent(definition, "id"); diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index df93ca6..85ca998 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -46,8 +46,8 @@ export class PostmessageTransport extends EventEmitter implements ITransport { private _ready = false; private _widgetId: string | null = null; - private outboundRequests = new Map(); - private stopController = new AbortController(); + private readonly outboundRequests = new Map(); + private readonly stopController = new AbortController(); public get ready(): boolean { return this._ready; @@ -58,10 +58,10 @@ export class PostmessageTransport extends EventEmitter implements ITransport { } public constructor( - private sendDirection: WidgetApiDirection, - private initialWidgetId: string | null, - private transportWindow: Window | typeof globalThis, - private inboundWindow: Window | typeof globalThis, + private readonly sendDirection: WidgetApiDirection, + private readonly initialWidgetId: string | null, + private readonly transportWindow: Window | typeof globalThis, + private readonly inboundWindow: Window | typeof globalThis, ) { super(); this._widgetId = initialWidgetId; diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index 3710242..a40d5a3 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -54,7 +54,7 @@ class WidgetTransportHelper { /** For ignoring the request sent by {@link WidgetApi.start} */ private skippedFirstRequest = false; - public constructor(private channels: TransportChannels) {} + public constructor(private readonly channels: TransportChannels) {} public nextTrackedRequest(): SendRequestArgs | undefined { if (!this.skippedFirstRequest) { @@ -70,7 +70,7 @@ class WidgetTransportHelper { } class ClientTransportHelper { - public constructor(private channels: TransportChannels) {} + public constructor(private readonly channels: TransportChannels) {} public trackRequest(action: WidgetApiFromWidgetAction, data: IWidgetApiRequestData): void { this.channels.requestQueue.push({ action, data }); From 41adacc7fbf506f5a6017361a8d85d3ab85ea1a6 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 3 Nov 2025 12:12:44 -0500 Subject: [PATCH 07/23] Tag MSC-related identifiers as `@experimental` --- src/WidgetApi.ts | 4 ++-- src/interfaces/Capabilities.ts | 15 +++++++++------ src/interfaces/WidgetApiAction.ts | 18 +++++++++--------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 99cf026..99771ee 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -481,7 +481,7 @@ export class WidgetApi extends EventEmitter { } /** - * @deprecated This currently relies on an unstable MSC (MSC4157). + * @experimental This currently relies on an unstable MSC (MSC4157). */ public updateDelayedEvent( delayId: string, @@ -665,7 +665,7 @@ export class WidgetApi extends EventEmitter { * @param {string} uri The URI to navigate to. * @returns {Promise} Resolves when complete. * @throws Throws if the URI is invalid or cannot be processed. - * @deprecated This currently relies on an unstable MSC (MSC2931). + * @experimental This currently relies on an unstable MSC (MSC2931). */ public navigateTo(uri: string): Promise { if (!uri || !uri.startsWith("https://matrix.to/#")) { diff --git a/src/interfaces/Capabilities.ts b/src/interfaces/Capabilities.ts index f541ac5..9b1ec15 100644 --- a/src/interfaces/Capabilities.ts +++ b/src/interfaces/Capabilities.ts @@ -26,28 +26,31 @@ export enum MatrixCapabilities { */ RequiresClient = "io.element.requires_client", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC2931Navigate = "org.matrix.msc2931.navigate", + /** + * @experimental It is not recommended to rely on this existing - it can be removed without notice. + */ MSC3846TurnServers = "town.robin.msc3846.turn_servers", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4039UploadFile = "org.matrix.msc4039.upload_file", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4039DownloadFile = "org.matrix.msc4039.download_file", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } diff --git a/src/interfaces/WidgetApiAction.ts b/src/interfaces/WidgetApiAction.ts index 2f0bcf5..b6ac75d 100644 --- a/src/interfaces/WidgetApiAction.ts +++ b/src/interfaces/WidgetApiAction.ts @@ -49,47 +49,47 @@ export enum WidgetApiFromWidgetAction { BeeperReadRoomAccountData = "com.beeper.read_room_account_data", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC2876ReadEvents = "org.matrix.msc2876.read_events", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC2931Navigate = "org.matrix.msc2931.navigate", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC2974RenegotiateCapabilities = "org.matrix.msc2974.request_capabilities", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC3869ReadRelations = "org.matrix.msc3869.read_relations", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4039UploadFileAction = "org.matrix.msc4039.upload_file", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4039DownloadFileAction = "org.matrix.msc4039.download_file", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * @experimental It is not recommended to rely on this existing - it can be removed without notice. */ MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } From d0c8b201d10f56fcd6403b20a23cfc49cbcf8365 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 3 Nov 2025 12:35:58 -0500 Subject: [PATCH 08/23] Optimize expression - Use optional chaining - Don't call toString on a string --- src/ClientWidgetApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 8f36cb0..94178ef 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -386,7 +386,7 @@ export class ClientWidgetApi extends EventEmitter { }); } - if (!request.data?.uri || !request.data?.uri.toString().startsWith("https://matrix.to/#")) { + if (!request.data?.uri.startsWith("https://matrix.to/#")) { return this.transport.reply(request, { error: { message: "Invalid matrix.to URI" }, }); From cfee6e27955da00a3ed86ef34a039e7cd97a7490 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 3 Nov 2025 12:46:50 -0500 Subject: [PATCH 09/23] Apply await-thenable lint rule --- .eslintrc.js | 1 + src/ClientWidgetApi.ts | 24 ++++++++++++------------ src/WidgetApi.ts | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ab91608..b5b7759 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,7 @@ module.exports = { asyncArrow: "always", }, ], + "@typescript-eslint/await-thenable": ["error"], "@typescript-eslint/prefer-readonly": ["error"], "arrow-parens": "off", "prefer-promise-reject-errors": "off", diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 94178ef..fcfec86 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -714,25 +714,25 @@ export class ClientWidgetApi extends EventEmitter { private async handleSendToDevice(request: ISendToDeviceFromWidgetActionRequest): Promise { if (!request.data.type) { - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "Invalid request - missing event type" }, }); } else if (!request.data.messages) { - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "Invalid request - missing event contents" }, }); } else if (typeof request.data.encrypted !== "boolean") { - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "Invalid request - missing encryption flag" }, }); } else if (!this.canSendToDeviceEvent(request.data.type)) { - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "Cannot send to-device events of this type" }, }); } else { try { await this.driver.sendToDevice(request.data.type, request.data.encrypted, request.data.messages); - await this.transport.reply(request, {}); + this.transport.reply(request, {}); } catch (e) { console.error("error sending to-device event", e); this.handleDriverError(e, request, "Error sending event"); @@ -761,12 +761,12 @@ export class ClientWidgetApi extends EventEmitter { private async handleWatchTurnServers(request: IWatchTurnServersRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) { - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "Missing capability" }, }); } else if (this.turnServers) { // We're already polling, so this is a no-op - await this.transport.reply(request, {}); + this.transport.reply(request, {}); } else { try { const turnServers = this.driver.getTurnServers(); @@ -775,14 +775,14 @@ export class ClientWidgetApi extends EventEmitter { // client isn't banned from getting TURN servers entirely const { done, value } = await turnServers.next(); if (done) throw new Error("Client refuses to provide any TURN servers"); - await this.transport.reply(request, {}); + this.transport.reply(request, {}); // Start the poll loop, sending the widget the initial result this.pollTurnServers(turnServers, value); this.turnServers = turnServers; } catch (e) { console.error("error getting first TURN server results", e); - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "TURN servers not available" }, }); } @@ -791,17 +791,17 @@ export class ClientWidgetApi extends EventEmitter { private async handleUnwatchTurnServers(request: IUnwatchTurnServersRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) { - await this.transport.reply(request, { + this.transport.reply(request, { error: { message: "Missing capability" }, }); } else if (!this.turnServers) { // We weren't polling anyways, so this is a no-op - await this.transport.reply(request, {}); + this.transport.reply(request, {}); } else { // Stop the generator, allowing it to clean up await this.turnServers.return(undefined); this.turnServers = null; - await this.transport.reply(request, {}); + this.transport.reply(request, {}); } } diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 99771ee..6108687 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -688,7 +688,7 @@ export class WidgetApi extends EventEmitter { const onUpdateTurnServers = async (ev: CustomEvent): Promise => { ev.preventDefault(); setTurnServer(ev.detail.data); - await this.transport.reply(ev.detail, {}); + this.transport.reply(ev.detail, {}); }; // Start listening for updates before we even start watching, to catch From dc845ebe26073aa7ca7701904a3853789be1d7cf Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 3 Nov 2025 14:06:27 -0500 Subject: [PATCH 10/23] Apply no-duplicate-imports lint rule --- .eslintrc.js | 1 + src/models/Widget.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b5b7759..565a7de 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,7 @@ module.exports = { browser: true, }, rules: { + "no-duplicate-imports": ["error"], "no-var": ["warn"], "prefer-rest-params": ["warn"], "prefer-spread": ["warn"], diff --git a/src/models/Widget.ts b/src/models/Widget.ts index 6d5bde0..0207b87 100644 --- a/src/models/Widget.ts +++ b/src/models/Widget.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { IWidget, IWidgetData, WidgetType } from ".."; +import { ITemplateParams, IWidget, IWidgetData, runTemplate, WidgetType } from ".."; import { assertPresent } from "./validation/utils"; -import { ITemplateParams, runTemplate } from ".."; /** * Represents the barest form of widget. From 35f08f42a14f6a06e89185d4ea162762240469de Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 3 Nov 2025 16:55:55 -0500 Subject: [PATCH 11/23] Remove useless assignment --- src/ClientWidgetApi.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index fcfec86..df1351c 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -62,7 +62,6 @@ import { } from "./interfaces/SendToDeviceAction"; import { EventDirection, EventKind, WidgetEventCapability } from "./models/WidgetEventCapability"; import { IRoomEvent } from "./interfaces/IRoomEvent"; -import { IRoomAccountData } from "./interfaces/IRoomAccountData"; import { IGetOpenIDActionRequest, IGetOpenIDActionResponseData, @@ -472,9 +471,9 @@ export class ClientWidgetApi extends EventEmitter { this.driver.askOpenID(observer); } + private handleReadRoomAccountData(request: IReadRoomAccountDataFromWidgetActionRequest): void | Promise { - let events: Promise = Promise.resolve([]); - events = this.driver.readRoomAccountData(request.data.type); + const events = this.driver.readRoomAccountData(request.data.type); if (!this.canReceiveRoomAccountData(request.data.type)) { return this.transport.reply(request, { From 2cdfb2859925773d6392f019eb0b7a9f66d5942f Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 09:13:57 -0500 Subject: [PATCH 12/23] More MSC-related identifiers as `@experimental` --- src/WidgetApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 2340c8f..23125f9 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -494,7 +494,7 @@ export class WidgetApi extends EventEmitter { } /** - * @deprecated This currently relies on an unstable MSC (MSC4157). + * @experimental This currently relies on an unstable MSC (MSC4157). */ public restartScheduledDelayedEvent(delayId: string): Promise { return this.transport.send( @@ -507,7 +507,7 @@ export class WidgetApi extends EventEmitter { } /** - * @deprecated This currently relies on an unstable MSC (MSC4157). + * @experimental This currently relies on an unstable MSC (MSC4157). */ public sendScheduledDelayedEvent(delayId: string): Promise { return this.transport.send( From 559864afd2971d352a6f12214969b6678f59b5f7 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 09:20:33 -0500 Subject: [PATCH 13/23] Migrate from deprecated Jest methods --- test/ClientWidgetApi-test.ts | 166 +++++++++++++++++------------------ 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 1a49390..08f02f1 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -214,12 +214,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Missing capability" }, }); }); - expect(driver.navigate).not.toBeCalled(); + expect(driver.navigate).not.toHaveBeenCalled(); }); it("fails to navigate to an unsupported URI", async () => { @@ -238,12 +238,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid matrix.to URI" }, }); }); - expect(driver.navigate).not.toBeCalled(); + expect(driver.navigate).not.toHaveBeenCalled(); }); it("should reject requests when the driver throws an exception", async () => { @@ -264,7 +264,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error handling navigation" }, }); }); @@ -294,7 +294,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error handling navigation", matrix_api_error: { @@ -416,7 +416,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error sending event" }, }); }); @@ -453,7 +453,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error sending event", matrix_api_error: { @@ -498,12 +498,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.sendDelayedEvent).not.toBeCalled(); + expect(driver.sendDelayedEvent).not.toHaveBeenCalled(); }); it("sends delayed message events", async () => { @@ -633,7 +633,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error sending event" }, }); }); @@ -673,7 +673,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error sending event", matrix_api_error: { @@ -922,12 +922,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.cancelScheduledDelayedEvent).not.toBeCalled(); + expect(driver.cancelScheduledDelayedEvent).not.toHaveBeenCalled(); }); it("fails to restart delayed events", async () => { @@ -947,12 +947,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.restartScheduledDelayedEvent).not.toBeCalled(); + expect(driver.restartScheduledDelayedEvent).not.toHaveBeenCalled(); }); it("fails to send delayed events", async () => { @@ -972,12 +972,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.sendScheduledDelayedEvent).not.toBeCalled(); + expect(driver.sendScheduledDelayedEvent).not.toHaveBeenCalled(); }); it("fails to update delayed events with unsupported action", async () => { @@ -997,14 +997,14 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.cancelScheduledDelayedEvent).not.toBeCalled(); - expect(driver.restartScheduledDelayedEvent).not.toBeCalled(); - expect(driver.sendScheduledDelayedEvent).not.toBeCalled(); + expect(driver.cancelScheduledDelayedEvent).not.toHaveBeenCalled(); + expect(driver.restartScheduledDelayedEvent).not.toHaveBeenCalled(); + expect(driver.sendScheduledDelayedEvent).not.toHaveBeenCalled(); }); it("can cancel delayed events", async () => { @@ -1101,7 +1101,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error updating delayed event" }, }); }); @@ -1132,7 +1132,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error updating delayed event", matrix_api_error: { @@ -1209,12 +1209,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - missing event type" }, }); }); - expect(driver.sendToDevice).not.toBeCalled(); + expect(driver.sendToDevice).not.toHaveBeenCalled(); }); it("fails to send to-device events without event contents", async () => { @@ -1234,12 +1234,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - missing event contents" }, }); }); - expect(driver.sendToDevice).not.toBeCalled(); + expect(driver.sendToDevice).not.toHaveBeenCalled(); }); it("fails to send to-device events without encryption flag", async () => { @@ -1265,12 +1265,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - missing encryption flag" }, }); }); - expect(driver.sendToDevice).not.toBeCalled(); + expect(driver.sendToDevice).not.toHaveBeenCalled(); }); it("fails to send to-device events with any event type", async () => { @@ -1297,12 +1297,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Cannot send to-device events of this type" }, }); }); - expect(driver.sendToDevice).not.toBeCalled(); + expect(driver.sendToDevice).not.toHaveBeenCalled(); }); it("should reject requests when the driver throws an exception", async () => { @@ -1333,7 +1333,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error sending event" }, }); }); @@ -1371,7 +1371,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Error sending event", matrix_api_error: { @@ -1685,12 +1685,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.readRoomTimeline).not.toBeCalled(); + expect(driver.readRoomTimeline).not.toHaveBeenCalled(); }); it("reads state events with a specific state key", async () => { @@ -1785,12 +1785,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: expect.any(String) }, }); }); - expect(driver.readRoomTimeline).not.toBeCalled(); + expect(driver.readRoomTimeline).not.toHaveBeenCalled(); }); }); @@ -1806,7 +1806,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3869]), }); }); @@ -1829,12 +1829,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { chunk: [createRoomEvent()], }); }); - expect(driver.readEventRelations).toBeCalledWith( + expect(driver.readEventRelations).toHaveBeenCalledWith( "$event", undefined, undefined, @@ -1872,12 +1872,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { chunk: [createRoomEvent(), createRoomEvent({ type: "net.example.test", state_key: "A" })], }); }); - expect(driver.readEventRelations).toBeCalledWith( + expect(driver.readEventRelations).toHaveBeenCalledWith( "$event", undefined, undefined, @@ -1916,12 +1916,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { chunk: [], }); }); - expect(driver.readEventRelations).toBeCalledWith( + expect(driver.readEventRelations).toHaveBeenCalledWith( "$event", "!room-id", "m.reference", @@ -1944,7 +1944,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - missing event ID" }, }); }); @@ -1963,7 +1963,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - limit out of range" }, }); }); @@ -1982,7 +1982,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unable to access room timeline: !another-room-id" }, }); }); @@ -2005,7 +2005,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while reading relations" }, }); }); @@ -2033,7 +2033,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while reading relations", matrix_api_error: { @@ -2064,7 +2064,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3973]), }); }); @@ -2092,7 +2092,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { limited: true, results: [ { @@ -2104,7 +2104,7 @@ describe("ClientWidgetApi", () => { }); }); - expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined); + expect(driver.searchUserDirectory).toHaveBeenCalledWith("foo", undefined); }); it("should accept all options and pass it to the driver", async () => { @@ -2138,7 +2138,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { limited: false, results: [ { @@ -2155,7 +2155,7 @@ describe("ClientWidgetApi", () => { }); }); - expect(driver.searchUserDirectory).toBeCalledWith("foo", 5); + expect(driver.searchUserDirectory).toHaveBeenCalledWith("foo", 5); }); it("should accept empty search_term", async () => { @@ -2177,13 +2177,13 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { limited: false, results: [], }); }); - expect(driver.searchUserDirectory).toBeCalledWith("", undefined); + expect(driver.searchUserDirectory).toHaveBeenCalledWith("", undefined); }); it("should reject requests when the capability was not requested", async () => { @@ -2197,11 +2197,11 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Missing capability" }, }); - expect(driver.searchUserDirectory).not.toBeCalled(); + expect(driver.searchUserDirectory).not.toHaveBeenCalled(); }); it("should reject requests without search_term", async () => { @@ -2217,11 +2217,11 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - missing search term" }, }); - expect(driver.searchUserDirectory).not.toBeCalled(); + expect(driver.searchUserDirectory).not.toHaveBeenCalled(); }); it("should reject requests with a negative limit", async () => { @@ -2240,11 +2240,11 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Invalid request - limit out of range" }, }); - expect(driver.searchUserDirectory).not.toBeCalled(); + expect(driver.searchUserDirectory).not.toHaveBeenCalled(); }); it("should reject requests when the driver throws an exception", async () => { @@ -2263,7 +2263,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while searching in the user directory" }, }); }); @@ -2292,7 +2292,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while searching in the user directory", matrix_api_error: { @@ -2324,7 +2324,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), }); }); @@ -2347,12 +2347,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { "m.upload.size": 1000, }); }); - expect(driver.getMediaConfig).toBeCalled(); + expect(driver.getMediaConfig).toHaveBeenCalled(); }); it("should reject requests when the capability was not requested", async () => { @@ -2366,11 +2366,11 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Missing capability" }, }); - expect(driver.getMediaConfig).not.toBeCalled(); + expect(driver.getMediaConfig).not.toHaveBeenCalled(); }); it("should reject requests when the driver throws an exception", async () => { @@ -2389,7 +2389,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while getting the media configuration" }, }); }); @@ -2418,7 +2418,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while getting the media configuration", matrix_api_error: { @@ -2450,7 +2450,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), }); }); @@ -2477,12 +2477,12 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { content_uri: "mxc://...", }); }); - expect(driver.uploadFile).toBeCalled(); + expect(driver.uploadFile).toHaveBeenCalled(); }); it("should reject requests when the capability was not requested", async () => { @@ -2498,11 +2498,11 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Missing capability" }, }); - expect(driver.uploadFile).not.toBeCalled(); + expect(driver.uploadFile).not.toHaveBeenCalled(); }); it("should reject requests when the driver throws an exception", async () => { @@ -2523,7 +2523,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while uploading a file" }, }); }); @@ -2554,7 +2554,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while uploading a file", matrix_api_error: { @@ -2616,11 +2616,11 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Missing capability" }, }); - expect(driver.uploadFile).not.toBeCalled(); + expect(driver.uploadFile).not.toHaveBeenCalled(); }); it("should reject requests when the driver throws an exception", async () => { @@ -2641,7 +2641,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while downloading a file" }, }); }); @@ -2672,7 +2672,7 @@ describe("ClientWidgetApi", () => { emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { + expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Unexpected error while downloading a file", matrix_api_error: { From 3359674e569fa6ad32b7036a12f49c5e487c575f Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 09:21:31 -0500 Subject: [PATCH 14/23] Fix tested delay parent ID arguments --- test/WidgetApi-test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index 28df491..96c0cd0 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -335,10 +335,12 @@ describe("WidgetApi", () => { delay_id: "id", } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, "parent-id")).resolves.toEqual( + { + room_id: "!room-id", + delay_id: "id", + }, + ); }); it("sends delayed child action state events", async () => { @@ -348,7 +350,7 @@ describe("WidgetApi", () => { } as ISendEventFromWidgetResponseData); await expect( - widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 1000, undefined), + widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 1000, "parent-id"), ).resolves.toEqual({ room_id: "!room-id", delay_id: "id", @@ -360,7 +362,7 @@ describe("WidgetApi", () => { error: { message: "An error occurred" }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000)).rejects.toThrow( "An error occurred", ); }); @@ -385,7 +387,7 @@ describe("WidgetApi", () => { }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000)).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), ); }); From 72350063ae562470dca255ab50d24292a1659822 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 10:27:02 -0500 Subject: [PATCH 15/23] Improve code coverage --- test/ClientWidgetApi-test.ts | 30 ++++++++++++++++++------------ test/WidgetApi-test.ts | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 08f02f1..9a07d1b 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -506,9 +506,12 @@ describe("ClientWidgetApi", () => { expect(driver.sendDelayedEvent).not.toHaveBeenCalled(); }); - it("sends delayed message events", async () => { + it.each([ + { hasDelay: true, hasParent: false }, + { hasDelay: false, hasParent: true }, + { hasDelay: true, hasParent: true }, + ])("sends delayed message events (hasDelay = $hasDelay, hasParent = $hasParent)", async ({ hasDelay, hasParent }) => { const roomId = "!room:example.org"; - const parentDelayId = "fp"; const timeoutDelayId = "ft"; driver.sendDelayedEvent.mockResolvedValue({ @@ -525,8 +528,8 @@ describe("ClientWidgetApi", () => { type: "m.room.message", content: {}, room_id: roomId, - delay: 5000, - parent_delay_id: parentDelayId, + ...(hasDelay && { delay: 5000 }), + ...(hasParent && { parent_delay_id: "fp" }), }, }; @@ -546,8 +549,8 @@ describe("ClientWidgetApi", () => { }); expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay, - event.data.parent_delay_id, + event.data.delay ?? null, + event.data.parent_delay_id ?? null, event.data.type, event.data.content, null, @@ -555,9 +558,12 @@ describe("ClientWidgetApi", () => { ); }); - it("sends delayed state events", async () => { + it.each([ + { hasDelay: true, hasParent: false }, + { hasDelay: false, hasParent: true }, + { hasDelay: true, hasParent: true }, + ])("sends delayed state events (hasDelay = $hasDelay, hasParent = $hasParent)", async ({ hasDelay, hasParent }) => { const roomId = "!room:example.org"; - const parentDelayId = "fp"; const timeoutDelayId = "ft"; driver.sendDelayedEvent.mockResolvedValue({ @@ -575,8 +581,8 @@ describe("ClientWidgetApi", () => { content: {}, state_key: "", room_id: roomId, - delay: 5000, - parent_delay_id: parentDelayId, + ...(hasDelay && { delay: 5000 }), + ...(hasParent && { parent_delay_id: "fp" }), }, }; @@ -596,8 +602,8 @@ describe("ClientWidgetApi", () => { }); expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay, - event.data.parent_delay_id, + event.data.delay ?? null, + event.data.parent_delay_id ?? null, event.data.type, event.data.content, "", diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index 96c0cd0..d9f29d0 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -770,4 +770,23 @@ describe("WidgetApi", () => { ); }); }); + + describe("capabilities", () => { + it("should request single capability", () => { + const capability = "org.example.capability"; + widgetApi.requestCapability(capability); + expect(widgetApi.hasCapability(capability)); + }); + + it("should request multiple capability", () => { + const capabilities: string[] = []; + for (let i = 1; i <= 3; i++) { + capabilities.push(`org.example.capability${i}`); + } + widgetApi.requestCapabilities(capabilities); + for (const capability of capabilities) { + expect(widgetApi.hasCapability(capability)); + } + }); + }); }); From 2ddd1bbc65ec554b158f443b198cefa2c1a059bc Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 10:29:09 -0500 Subject: [PATCH 16/23] Format with Prettier --- test/ClientWidgetApi-test.ts | 176 ++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 85 deletions(-) diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 9a07d1b..22c879a 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -507,109 +507,115 @@ describe("ClientWidgetApi", () => { }); it.each([ - { hasDelay: true, hasParent: false }, + { hasDelay: true, hasParent: false }, { hasDelay: false, hasParent: true }, - { hasDelay: true, hasParent: true }, - ])("sends delayed message events (hasDelay = $hasDelay, hasParent = $hasParent)", async ({ hasDelay, hasParent }) => { - const roomId = "!room:example.org"; - const timeoutDelayId = "ft"; - - driver.sendDelayedEvent.mockResolvedValue({ - roomId, - delayId: timeoutDelayId, - }); + { hasDelay: true, hasParent: true }, + ])( + "sends delayed message events (hasDelay = $hasDelay, hasParent = $hasParent)", + async ({ hasDelay, hasParent }) => { + const roomId = "!room:example.org"; + const timeoutDelayId = "ft"; + + driver.sendDelayedEvent.mockResolvedValue({ + roomId, + delayId: timeoutDelayId, + }); - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - room_id: roomId, - ...(hasDelay && { delay: 5000 }), - ...(hasParent && { parent_delay_id: "fp" }), - }, - }; + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + room_id: roomId, + ...(hasDelay && { delay: 5000 }), + ...(hasParent && { parent_delay_id: "fp" }), + }, + }; - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - delay_id: timeoutDelayId, + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + delay_id: timeoutDelayId, + }); }); - }); - expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay ?? null, - event.data.parent_delay_id ?? null, - event.data.type, - event.data.content, - null, - roomId, - ); - }); + expect(driver.sendDelayedEvent).toHaveBeenCalledWith( + event.data.delay ?? null, + event.data.parent_delay_id ?? null, + event.data.type, + event.data.content, + null, + roomId, + ); + }, + ); it.each([ { hasDelay: true, hasParent: false }, { hasDelay: false, hasParent: true }, { hasDelay: true, hasParent: true }, - ])("sends delayed state events (hasDelay = $hasDelay, hasParent = $hasParent)", async ({ hasDelay, hasParent }) => { - const roomId = "!room:example.org"; - const timeoutDelayId = "ft"; - - driver.sendDelayedEvent.mockResolvedValue({ - roomId, - delayId: timeoutDelayId, - }); + ])( + "sends delayed state events (hasDelay = $hasDelay, hasParent = $hasParent)", + async ({ hasDelay, hasParent }) => { + const roomId = "!room:example.org"; + const timeoutDelayId = "ft"; + + driver.sendDelayedEvent.mockResolvedValue({ + roomId, + delayId: timeoutDelayId, + }); - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.topic", - content: {}, - state_key: "", - room_id: roomId, - ...(hasDelay && { delay: 5000 }), - ...(hasParent && { parent_delay_id: "fp" }), - }, - }; + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.topic", + content: {}, + state_key: "", + room_id: roomId, + ...(hasDelay && { delay: 5000 }), + ...(hasParent && { parent_delay_id: "fp" }), + }, + }; - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.state_event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.state_event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - delay_id: timeoutDelayId, + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + delay_id: timeoutDelayId, + }); }); - }); - expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay ?? null, - event.data.parent_delay_id ?? null, - event.data.type, - event.data.content, - "", - roomId, - ); - }); + expect(driver.sendDelayedEvent).toHaveBeenCalledWith( + event.data.delay ?? null, + event.data.parent_delay_id ?? null, + event.data.type, + event.data.content, + "", + roomId, + ); + }, + ); it("should reject requests when the driver throws an exception", async () => { const roomId = "!room:example.org"; From b1cde39217f224b307bb00d677ccaed982b76c38 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 10:44:49 -0500 Subject: [PATCH 17/23] Set `noUnusedLocals` TSConfig option --- src/ClientWidgetApi.ts | 2 +- src/WidgetApi.ts | 2 +- src/transport/PostmessageTransport.ts | 2 +- tsconfig.json | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 919d36b..87f4cff 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -161,7 +161,7 @@ export class ClientWidgetApi extends EventEmitter { */ public constructor( public readonly widget: Widget, - private readonly iframe: HTMLIFrameElement, + iframe: HTMLIFrameElement, private readonly driver: WidgetDriver, ) { super(); diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 23125f9..f7137df 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -144,7 +144,7 @@ export class WidgetApi extends EventEmitter { */ public constructor( widgetId: string | null = null, - private readonly clientOrigin: string | null = null, + clientOrigin: string | null = null, ) { super(); if (!globalThis.parent) { diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index 85ca998..65791f9 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -59,7 +59,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { public constructor( private readonly sendDirection: WidgetApiDirection, - private readonly initialWidgetId: string | null, + initialWidgetId: string | null, private readonly transportWindow: Window | typeof globalThis, private readonly inboundWindow: Window | typeof globalThis, ) { diff --git a/tsconfig.json b/tsconfig.json index e5261eb..075bb62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "declaration": true, "types": ["jest"], "lib": ["es2020", "dom"], - "strict": true + "strict": true, + "noUnusedLocals": true }, "include": ["./src/**/*.ts"] } From 20ba7dad1edef2e189230a88bdf236393ee80e8b Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 10:46:38 -0500 Subject: [PATCH 18/23] Format with Prettier --- src/WidgetApi.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index f7137df..73ee411 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -142,10 +142,7 @@ export class WidgetApi extends EventEmitter { * the API will use the widget ID from the first valid request it receives. * @param {string} clientOrigin The origin of the client, or null if not known. */ - public constructor( - widgetId: string | null = null, - clientOrigin: string | null = null, - ) { + public constructor(widgetId: string | null = null, clientOrigin: string | null = null) { super(); if (!globalThis.parent) { throw new Error("No parent window. This widget doesn't appear to be embedded properly."); From 85ebbf4b0e854b2ed1c5564a87a4c9ad843ff90b Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 10:52:42 -0500 Subject: [PATCH 19/23] Actually test capabilities --- test/WidgetApi-test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index d9f29d0..4fd0a8d 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -774,8 +774,9 @@ describe("WidgetApi", () => { describe("capabilities", () => { it("should request single capability", () => { const capability = "org.example.capability"; + expect(widgetApi.hasCapability(capability)).toBe(false); widgetApi.requestCapability(capability); - expect(widgetApi.hasCapability(capability)); + expect(widgetApi.hasCapability(capability)).toBe(true); }); it("should request multiple capability", () => { @@ -783,9 +784,12 @@ describe("WidgetApi", () => { for (let i = 1; i <= 3; i++) { capabilities.push(`org.example.capability${i}`); } + for (const capability of capabilities) { + expect(widgetApi.hasCapability(capability)).toBe(false); + } widgetApi.requestCapabilities(capabilities); for (const capability of capabilities) { - expect(widgetApi.hasCapability(capability)); + expect(widgetApi.hasCapability(capability)).toBe(true); } }); }); From bcf05c131ae47263d4d7964d9f616b6e6cde5e54 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 11:22:28 -0500 Subject: [PATCH 20/23] Remove overwritten assignment --- src/transport/PostmessageTransport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index 65791f9..361e66b 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -45,7 +45,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { public timeoutSeconds = 10; private _ready = false; - private _widgetId: string | null = null; + private _widgetId: string | null; private readonly outboundRequests = new Map(); private readonly stopController = new AbortController(); From 496f0dec8e423648917c7b2569f4477c601f022b Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 16:19:02 -0500 Subject: [PATCH 21/23] Add code coverage for requests over postMessage --- test/WidgetApi-test.ts | 121 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index 4fd0a8d..09bde09 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -794,3 +794,124 @@ describe("WidgetApi", () => { }); }); }); + +describe("postMessage", () => { + let widgetApi: WidgetApi; + const onHandledRequest = jest.fn(); + + let afterMessage: Promise; + let afterMessageListener: () => void; + + beforeEach(() => { + widgetApi = new WidgetApi(); + widgetApi.start(); + + onHandledRequest.mockClear(); + widgetApi.transport.on("message", onHandledRequest); + + ({ promise: afterMessage, resolve: afterMessageListener } = Promise.withResolvers()); + afterMessage = new Promise((resolve) => { + afterMessageListener = resolve; + globalThis.addEventListener("message", afterMessageListener); + }); + }); + + afterEach(() => { + globalThis.removeEventListener("message", afterMessageListener); + widgetApi.transport.removeAllListeners(); + }); + + async function postMessage(message: unknown): Promise { + globalThis.postMessage(message, "*"); + await afterMessage; + } + + it("should handle a valid request", async () => { + const action = "a"; + widgetApi.on(`action:${action}`, (ev: Event) => { + ev.preventDefault(); + }); + + await postMessage({ + api: WidgetApiDirection.ToWidget, + requestId: "rid", + action, + widgetId: "w", + data: {}, + } satisfies IWidgetApiRequest); + expect(onHandledRequest).toHaveBeenCalled(); + }); + + it("should drop a request with the wrong direction", async () => { + await postMessage({ + api: WidgetApiDirection.FromWidget, + requestId: "rid", + action: "a", + widgetId: "w", + data: {}, + } satisfies IWidgetApiRequest); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); + + it("should drop a request without an action", async () => { + await postMessage({ + api: WidgetApiDirection.ToWidget, + requestId: "rid", + widgetId: "w", + data: {}, + } satisfies Omit); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); + + it("should drop a request without an request ID", async () => { + await postMessage({ + api: WidgetApiDirection.ToWidget, + action: "a", + widgetId: "w", + data: {}, + } satisfies Omit); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); + + it("should drop a request without an widget ID", async () => { + await postMessage({ + api: WidgetApiDirection.ToWidget, + action: "a", + requestId: "rid", + data: {}, + } satisfies Omit); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); + + it("should enforce strictOriginCheck", async () => { + // the generated MessageEvent will have an empty origin + widgetApi.transport.strictOriginCheck = true; + + await postMessage({ + api: WidgetApiDirection.ToWidget, + requestId: "rid", + action: "a", + widgetId: "w", + data: {}, + } satisfies IWidgetApiRequest); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); + + it("should drop a null message", async () => { + await postMessage(null); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); + + it("should drop a request after having aborted", async () => { + widgetApi.transport.stop(); + + await postMessage({ + api: WidgetApiDirection.ToWidget, + requestId: "rid", + action: "a", + widgetId: "w", + data: {}, + } satisfies IWidgetApiRequest); + expect(onHandledRequest).not.toHaveBeenCalled(); + }); +}); From 19a6a24336a836b2e64b3243c69effb0dfbdf2a3 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 16:19:30 -0500 Subject: [PATCH 22/23] Move capabilities test to its own suite Otherwise it break the postMessage tests --- test/WidgetApi-test.ts | 44 ++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index 09bde09..94b6bdc 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -770,28 +770,34 @@ describe("WidgetApi", () => { ); }); }); +}); + +describe("capabilities", () => { + let widgetApi: WidgetApi; + + beforeEach(() => { + widgetApi = new WidgetApi(); + }); + + it("should request single capability", () => { + const capability = "org.example.capability"; + expect(widgetApi.hasCapability(capability)).toBe(false); + widgetApi.requestCapability(capability); + expect(widgetApi.hasCapability(capability)).toBe(true); + }); - describe("capabilities", () => { - it("should request single capability", () => { - const capability = "org.example.capability"; + it("should request multiple capabilities", () => { + const capabilities: string[] = []; + for (let i = 1; i <= 3; i++) { + capabilities.push(`org.example.capability${i}`); + } + for (const capability of capabilities) { expect(widgetApi.hasCapability(capability)).toBe(false); - widgetApi.requestCapability(capability); + } + widgetApi.requestCapabilities(capabilities); + for (const capability of capabilities) { expect(widgetApi.hasCapability(capability)).toBe(true); - }); - - it("should request multiple capability", () => { - const capabilities: string[] = []; - for (let i = 1; i <= 3; i++) { - capabilities.push(`org.example.capability${i}`); - } - for (const capability of capabilities) { - expect(widgetApi.hasCapability(capability)).toBe(false); - } - widgetApi.requestCapabilities(capabilities); - for (const capability of capabilities) { - expect(widgetApi.hasCapability(capability)).toBe(true); - } - }); + } }); }); From 77bef30a23625cd2c87d74637c2671d29c02a253 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 4 Nov 2025 16:34:16 -0500 Subject: [PATCH 23/23] De-negate response/request check --- src/transport/PostmessageTransport.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index 361e66b..b1c7125 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -165,15 +165,13 @@ export class PostmessageTransport extends EventEmitter implements ITransport { const response = ev.data; if (!response.action || !response.requestId || !response.widgetId) return; // invalid request/response - if (!response.response) { - // it's a request + if (response.response) { + if (response.api !== this.sendDirection) return; // wrong direction + this.handleResponse(response); + } else { const request = response; if (request.api !== invertedDirection(this.sendDirection)) return; // wrong direction this.handleRequest(request); - } else { - // it's a response - if (response.api !== this.sendDirection) return; // wrong direction - this.handleResponse(response); } }