From e936ba7d8248b5ca59cb8821b2e3dc207ecb854e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Mar 2026 10:46:28 +0000 Subject: [PATCH] Also a test for the module entrypoint --- .../element-web/tests/index.test.ts | 175 ++++++++++++++++++ .../widget-toggles/element-web/tests/mocks.ts | 45 +++++ .../element-web/tests/toggle.test.tsx | 33 +--- 3 files changed, 222 insertions(+), 31 deletions(-) create mode 100644 modules/widget-toggles/element-web/tests/index.test.ts create mode 100644 modules/widget-toggles/element-web/tests/mocks.ts diff --git a/modules/widget-toggles/element-web/tests/index.test.ts b/modules/widget-toggles/element-web/tests/index.test.ts new file mode 100644 index 0000000000..d1258e56a2 --- /dev/null +++ b/modules/widget-toggles/element-web/tests/index.test.ts @@ -0,0 +1,175 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { type Api } from "@element-hq/element-web-module-api"; +import { type IWidget } from "matrix-widget-api"; + +import WidgetToggleModule from "../src/index"; +import { CONFIG_KEY, WidgetTogglesConfig } from "../src/config"; +import { mockWidget, mockWidgetApi } from "./mocks"; + +const makeApi = (widgets: IWidget[] = []): Api => { + const addRoomHeaderButtonCallback = vi.fn(); + return { + config: { + get: vi.fn().mockReturnValue({ types: ["m.custom"] }), + }, + extras: { + addRoomHeaderButtonCallback, + }, + widget: mockWidgetApi({ + getWidgetsInRoom: vi.fn().mockReturnValue(widgets), + }), + i18n: { + translate: vi.fn().mockImplementation((key: string) => key), + }, + } as unknown as Api; +}; + +vi.mock("../src/config", async () => { + return { + CONFIG_KEY: "fake_config_key", + WidgetTogglesConfig: { + parse: vi.fn(), + }, + }; +}); + +describe("WidgetToggleModule", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("load", () => { + test("reads config using CONFIG_KEY", async () => { + const api = makeApi(); + + const module = new WidgetToggleModule(api); + await module.load(); + + expect(api.config.get).toHaveBeenCalledWith(CONFIG_KEY); + }); + + test("parses config with WidgetTogglesConfig.parse", async () => { + const api = makeApi(); + const rawConfig = { types: ["m.custom"] }; + (api.config.get as ReturnType).mockReturnValue(rawConfig); + + const module = new WidgetToggleModule(api); + await module.load(); + + expect(WidgetTogglesConfig.parse).toHaveBeenCalledWith(rawConfig); + }); + + test("registers a room header button callback", async () => { + const api = makeApi(); + (WidgetTogglesConfig.parse as ReturnType).mockReturnValue({ types: ["m.custom"] }); + + const module = new WidgetToggleModule(api); + await module.load(); + + expect(api.extras.addRoomHeaderButtonCallback).toHaveBeenCalledOnce(); + }); + + test("throws error when config parsing fails", async () => { + const api = makeApi(); + (WidgetTogglesConfig.parse as ReturnType).mockImplementation(() => { + throw new Error("Invalid config"); + }); + + const module = new WidgetToggleModule(api); + await expect(module.load()).rejects.toThrow("Errors in module configuration for widget toggles module"); + }); + }); + + describe("room header button callback", () => { + const roomId = "!room:example.com"; + + const getCallback = async (api: Api): Promise<(roomId: string) => React.JSX.Element | undefined> => { + (WidgetTogglesConfig.parse as ReturnType).mockReturnValue({ types: ["m.custom"] }); + const module = new WidgetToggleModule(api); + await module.load(); + return (api.extras.addRoomHeaderButtonCallback as ReturnType).mock.calls[0][0]; + }; + + test("returns undefined when there are no widgets in the room", async () => { + const api = makeApi([]); + const callback = await getCallback(api); + + const result = callback(roomId); + expect(result).toBeUndefined(); + }); + + test("returns undefined when no widgets match the configured types", async () => { + const api = makeApi([mockWidget({ type: "m.other" })]); + const callback = await getCallback(api); + + const result = callback(roomId); + expect(result).toBeUndefined(); + }); + + test("renders WidgetToggle for each matching widget", async () => { + const api = makeApi([ + mockWidget({ id: "w1", type: "m.custom", name: "Widget One" }), + mockWidget({ id: "w2", type: "m.custom", name: "Widget Two" }), + ]); + (api.i18n.translate as ReturnType).mockImplementation( + (key: string, vars?: Record) => { + let result = key; + if (vars) { + for (const [k, v] of Object.entries(vars)) { + result = result.replace(`%(${k})s`, v); + } + } + return result; + }, + ); + const callback = await getCallback(api); + + const result = callback(roomId); + expect(result).toBeDefined(); + render(result!); + + expect(screen.getAllByRole("button").length).toBe(2); + }); + + test("does not render WidgetToggle for non-matching widget types", async () => { + const api = makeApi([ + mockWidget({ id: "w1", type: "m.custom", name: "Widget One" }), + mockWidget({ id: "w2", type: "m.other", name: "Widget Other" }), + ]); + (api.i18n.translate as ReturnType).mockImplementation( + (key: string, vars?: Record) => { + let result = key; + if (vars) { + for (const [k, v] of Object.entries(vars)) { + result = result.replace(`%(${k})s`, v); + } + } + return result; + }, + ); + const callback = await getCallback(api); + + const result = callback(roomId); + expect(result).toBeDefined(); + render(result!); + + expect(screen.getAllByRole("button").length).toBe(1); + }); + + test("calls getWidgetsInRoom with correct roomId", async () => { + const api = makeApi([]); + const callback = await getCallback(api); + + callback(roomId); + expect(api.widget.getWidgetsInRoom).toHaveBeenCalledWith(roomId); + }); + }); +}); diff --git a/modules/widget-toggles/element-web/tests/mocks.ts b/modules/widget-toggles/element-web/tests/mocks.ts new file mode 100644 index 0000000000..e37cd67b01 --- /dev/null +++ b/modules/widget-toggles/element-web/tests/mocks.ts @@ -0,0 +1,45 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type I18nApi, type WidgetApi } from "@element-hq/element-web-module-api"; +import { type IWidget } from "matrix-widget-api"; +import { vi } from "vitest"; + +export function mockWidget(overrides: Partial = {}): IWidget { + return { + id: "widget-1", + creatorUserId: "@user:example.com", + type: "m.custom", + name: "My Widget", + url: "https://example.com", + ...overrides, + }; +} + +export function mockWidgetApi(overrides: Partial = {}): WidgetApi { + return { + getWidgetsInRoom: vi.fn().mockReturnValue([]), + getAppAvatarUrl: vi.fn().mockReturnValue(null), + isAppInContainer: vi.fn().mockReturnValue(false), + moveAppToContainer: vi.fn(), + ...overrides, + } as unknown as WidgetApi; +} + +export function mockI18nApi(): I18nApi { + return { + translate: vi.fn().mockImplementation((key: string, vars?: Record) => { + let result = key; + if (vars) { + for (const [k, v] of Object.entries(vars)) { + result = result.replace(`%(${k})s`, v); + } + } + return result; + }), + } as unknown as I18nApi; +} diff --git a/modules/widget-toggles/element-web/tests/toggle.test.tsx b/modules/widget-toggles/element-web/tests/toggle.test.tsx index 3f115b9579..0a0e40449f 100644 --- a/modules/widget-toggles/element-web/tests/toggle.test.tsx +++ b/modules/widget-toggles/element-web/tests/toggle.test.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, type vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { type WidgetApi, type I18nApi } from "@element-hq/element-web-module-api"; import { type IWidget } from "matrix-widget-api"; @@ -14,39 +14,10 @@ import { TooltipProvider } from "@vector-im/compound-web"; import userEvent from "@testing-library/user-event"; import { WidgetToggle } from "../src/toggle"; +import { mockI18nApi, mockWidget, mockWidgetApi } from "./mocks"; const roomId = "!room:example.com"; -const mockWidget = (overrides: Partial = {}): IWidget => ({ - id: "widget-1", - creatorUserId: "@user:example.com", - type: "m.custom", - name: "My Widget", - url: "https://example.com", - ...overrides, -}); - -const mockWidgetApi = (overrides: Partial = {}): WidgetApi => - ({ - getAppAvatarUrl: vi.fn().mockReturnValue(null), - isAppInContainer: vi.fn().mockReturnValue(false), - moveAppToContainer: vi.fn(), - ...overrides, - }) as unknown as WidgetApi; - -const mockI18nApi = (): I18nApi => - ({ - translate: vi.fn().mockImplementation((key: string, vars?: Record) => { - let result = key; - if (vars) { - for (const [k, v] of Object.entries(vars)) { - result = result.replace(`%(${k})s`, v); - } - } - return result; - }), - }) as unknown as I18nApi; - const wrapper = ({ children }: PropsWithChildren): React.JSX.Element => {children}; describe("WidgetToggle", () => {