From dd2a125d52760170d48857c91dbb10ebb136c036 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Mar 2026 18:51:40 +0000 Subject: [PATCH] Add unit test for the toggle component Using playwright-provided chromium via vitest + testing library --- .../widget-toggles/element-web/package.json | 4 + .../element-web/tests/setupTests.ts | 13 ++ .../element-web/tests/toggle.test.tsx | 111 ++++++++++++++++++ .../element-web/tests/tsconfig.json | 7 ++ .../element-web/vitest.config.ts | 10 +- 5 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 modules/widget-toggles/element-web/tests/setupTests.ts create mode 100644 modules/widget-toggles/element-web/tests/toggle.test.tsx create mode 100644 modules/widget-toggles/element-web/tests/tsconfig.json diff --git a/modules/widget-toggles/element-web/package.json b/modules/widget-toggles/element-web/package.json index 3ae308984a..b4f1a3e1bf 100644 --- a/modules/widget-toggles/element-web/package.json +++ b/modules/widget-toggles/element-web/package.json @@ -14,9 +14,13 @@ "devDependencies": { "@arcmantle/vite-plugin-import-css-sheet": "^1.0.12", "@element-hq/element-web-module-api": "^1.0.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^22.10.7", "@types/react": "^19", "@vitejs/plugin-react": "^5.0.0", + "@vitest/browser-playwright": "4.0.18", "@vitest/coverage-v8": "^4.0.0", "react": "^19", "rollup-plugin-external-globals": "^0.13.0", diff --git a/modules/widget-toggles/element-web/tests/setupTests.ts b/modules/widget-toggles/element-web/tests/setupTests.ts new file mode 100644 index 0000000000..d7601a1499 --- /dev/null +++ b/modules/widget-toggles/element-web/tests/setupTests.ts @@ -0,0 +1,13 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; + +afterEach(() => { + cleanup(); +}); diff --git a/modules/widget-toggles/element-web/tests/toggle.test.tsx b/modules/widget-toggles/element-web/tests/toggle.test.tsx new file mode 100644 index 0000000000..3f115b9579 --- /dev/null +++ b/modules/widget-toggles/element-web/tests/toggle.test.tsx @@ -0,0 +1,111 @@ +/* +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 WidgetApi, type I18nApi } from "@element-hq/element-web-module-api"; +import { type IWidget } from "matrix-widget-api"; +import { type PropsWithChildren } from "react"; +import { TooltipProvider } from "@vector-im/compound-web"; +import userEvent from "@testing-library/user-event"; + +import { WidgetToggle } from "../src/toggle"; + +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", () => { + let widgetApi: WidgetApi; + let i18nApi: I18nApi; + let app: IWidget; + + beforeEach(() => { + widgetApi = mockWidgetApi(); + i18nApi = mockI18nApi(); + app = mockWidget(); + }); + + test("displays avatar image when widget has an avatar URL", () => { + (widgetApi.getAppAvatarUrl as ReturnType).mockReturnValue("https://example.com/avatar.png"); + render(, { wrapper }); + const img = screen.getByRole("img", { name: app.name }); + expect(img).toBeDefined(); + expect(img.getAttribute("src")).toBe("https://example.com/avatar.png"); + }); + + test("renders the Jitsi avatar for Jitsi widgets", () => { + app = mockWidget({ type: "m.jitsi", name: "Jitsi" }); + render(, { wrapper }); + const img = screen.getByRole("img", { name: "Jitsi" }); + expect(img.getAttribute("src")).toMatch(/^data:image\/svg\+xml;base64,/); + }); + + test("shows 'Show' label when widget is not in container", () => { + (widgetApi.isAppInContainer as ReturnType).mockReturnValue(false); + render(, { wrapper }); + const button = screen.getByRole("button", { name: "Show My Widget" }); + expect(button).toBeDefined(); + }); + + test("shows 'Hide' label when widget is in container", () => { + (widgetApi.isAppInContainer as ReturnType).mockReturnValue(true); + render(, { wrapper }); + const button = screen.getByRole("button", { name: "Hide My Widget" }); + expect(button).toBeDefined(); + }); + + test("calls moveAppToContainer with 'top' when widget is not in container and button is clicked", async () => { + const user = userEvent.setup(); + + (widgetApi.isAppInContainer as ReturnType).mockReturnValue(false); + render(, { wrapper }); + const button = screen.getByRole("button", { name: "Show My Widget" }); + await user.click(button); + expect(widgetApi.moveAppToContainer).toHaveBeenCalledWith(app, "top", roomId); + }); + + test("calls moveAppToContainer with 'right' when widget is in container and button is clicked", async () => { + const user = userEvent.setup(); + + (widgetApi.isAppInContainer as ReturnType).mockReturnValue(true); + render(, { wrapper }); + const button = screen.getByRole("button", { name: "Hide My Widget" }); + await user.click(button); + expect(widgetApi.moveAppToContainer).toHaveBeenCalledWith(app, "right", roomId); + }); +}); diff --git a/modules/widget-toggles/element-web/tests/tsconfig.json b/modules/widget-toggles/element-web/tests/tsconfig.json new file mode 100644 index 0000000000..f5eb2ca359 --- /dev/null +++ b/modules/widget-toggles/element-web/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + }, + "include": ["."] +} diff --git a/modules/widget-toggles/element-web/vitest.config.ts b/modules/widget-toggles/element-web/vitest.config.ts index 783608fb44..4a5b5a32cc 100644 --- a/modules/widget-toggles/element-web/vitest.config.ts +++ b/modules/widget-toggles/element-web/vitest.config.ts @@ -7,12 +7,13 @@ Please see LICENSE files in the repository root for full details. import { defineConfig } from "vitest/config"; import { env } from "node:process"; +import { playwright } from "@vitest/browser-playwright"; const isGHA = env["GITHUB_ACTIONS"] !== undefined; export default defineConfig({ test: { - include: ["tests/**/*.test.ts"], + include: ["tests/**/*.test.{ts,tsx}"], exclude: ["./e2e/**/*", "./node_modules/**/*"], reporters: isGHA ? ["default", ["vitest-sonar-reporter", { outputFile: "coverage/sonar-report.xml" }]] @@ -22,5 +23,12 @@ export default defineConfig({ include: ["src/**/*.ts"], reporter: [["lcov", { projectRoot: "../../../" }], "text"], }, + browser: { + enabled: true, + headless: true, + provider: playwright({}), + instances: [{ browser: "chromium" }], + }, + setupFiles: ["tests/setupTests.ts"], }, });