Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ jobs:
- name: Install Deps
run: "yarn install --frozen-lockfile"

- name: Get installed Playwright version
id: playwright
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT

- name: Cache playwright binaries
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "yarn playwright install --with-deps --only-shell"

- name: Run tests
run: yarn test

Expand Down
3 changes: 3 additions & 0 deletions knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Please see LICENSE files in the repository root for full details.

import { KnipConfig } from "knip";

// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter`
process.env.GITHUB_ACTIONS = "1";

export default {
ignoreDependencies: [
// Needed for lint:workflows
Expand Down
2 changes: 1 addition & 1 deletion modules/banner/element-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@types/node": "^22.10.7",
"@types/react": "^19",
"@vitejs/plugin-react": "^5.0.0",
"react": "^19",
"react": "19.2.4",
"rollup-plugin-external-globals": "^0.13.0",
"typescript": "^5.7.3",
"vite": "^7.1.11",
Expand Down
4 changes: 2 additions & 2 deletions modules/opendesk/element-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"@types/node": "^22.10.7",
"@types/react": "^19",
"@vitejs/plugin-react": "^5.0.0",
"react": "^19",
"react-dom": "^19",
"react": "19.2.4",
"react-dom": "19.2.4",
"rollup-plugin-external-globals": "^0.13.0",
"typescript": "^5.7.3",
"vite": "^7.1.11",
Expand Down
2 changes: 1 addition & 1 deletion modules/restricted-guests/element-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@types/node": "^22.10.7",
"@types/react": "^19",
"@vitejs/plugin-react": "^5.0.0",
"react": "^19",
"react": "19.2.4",
"rollup-plugin-external-globals": "^0.13.0",
"typescript": "^5.7.3",
"vite": "^7.1.11",
Expand Down
16 changes: 16 additions & 0 deletions modules/widget-toggles/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Widget Toggles Module

Adds room header buttons for widgets in the room.

This module needs to be configured to control what widget types get buttons added for them.
The following config snippet enables the module and configures it to add buttons for both
custom and jitsi widgets:

```
"modules": [
"/modules/widget-toggles/lib/index.js"
],
"io.element.element-web-modules.widget-toggles": {
"types": ["m.custom", "jitsi"]
}
```
5 changes: 5 additions & 0 deletions modules/widget-toggles/element-web/e2e/fixture/widget.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<body>
<h1>This is the content of the widget</h1>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
170 changes: 170 additions & 0 deletions modules/widget-toggles/element-web/e2e/widget-toggles.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
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 Page } from "@playwright/test";
import { type Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api";
import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/HomeserverContainer";
import { type Container } from "@element-hq/element-web-module-api";

import { test as base, expect } from "../../../../playwright/element-web-test.ts";

const test = base.extend<{
// Resolver for when to respond to the navigation.json request
navigationJsonResolver: PromiseWithResolvers<void>;
}>({
navigationJsonResolver: async ({}, use) => {
await use(Promise.withResolvers<void>());
},
});

const TEST_WIDGET_NAME = "Name of the test widget";

async function makeRoomWithWidgetAndGoTo(
homeserver: StartedHomeserverContainer,
user: Credentials,
page: Page,
avatarUrl?: string,
): Promise<string> {
const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>(
"POST",
"/v3/createRoom",
user.accessToken,
{
name: "Come on in we've got widgets",
},
);
await homeserver.csApi.request<{
event_id: string;
}>("PUT", `/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`, user.accessToken, {
id: "1",
creatorUserId: user.userId,
type: "m.custom",
name: TEST_WIDGET_NAME,
url: `http://localhost:8080/widget.html`,
avatar_url: avatarUrl,
});

await page.goto(`/#/room/${roomId}`);

return roomId;
}

async function moveWidgetToContainer(
homeserver: StartedHomeserverContainer,
user: Credentials,
roomId: string,
container: Container,
): Promise<void> {
await homeserver.csApi.request(
"PUT",
`/v3/user/${encodeURIComponent(user.userId)}/rooms/${encodeURIComponent(roomId)}/account_data/im.vector.web.settings`,
user.accessToken,
{
"Widgets.layout": {
widgets: {
"1": { container },
},
},
},
);
}

test.describe("widget-toggles", () => {
test.use({
displayName: "Timmy",
page: async ({ context, page, moduleDir }, use) => {
await context.route("http://localhost:8080/widget.html*", async (route) => {
await route.fulfill({ path: `${moduleDir}/e2e/fixture/widget.html`, contentType: "text/html" });
});

await context.route("http://localhost:8080/wigeon.png", async (route) => {
await route.fulfill({ path: `${moduleDir}/e2e/fixture/wigeon.png`, contentType: "image/png" });
});

await page.goto("/");
await use(page);
},
});

test("should error if config is missing", async ({ page }) => {
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
await expect(page.getByText("Errors in module configuration")).toBeVisible();
// We don't take a screenshot as we don't want to assert Element's styling, only our own
});

test.describe("with correct config", () => {
test.use({
config: {
"io.element.element-web-modules.widget-toggles": {
types: ["m.custom"],
},
},
});

test(
"should render 'show' button for widget not in top",
{ tag: ["@screenshot"] },
async ({ homeserver, page, user }) => {
await makeRoomWithWidgetAndGoTo(homeserver, user, page);

await expect(page.getByRole("button", { name: "Show " + TEST_WIDGET_NAME })).toBeVisible();
},
);

test(
"should render 'hide' button for widget in top",
{ tag: ["@screenshot"] },
async ({ homeserver, page, user }) => {
const roomId = await makeRoomWithWidgetAndGoTo(homeserver, user, page);

await moveWidgetToContainer(homeserver, user, roomId, "top");

await expect(page.getByRole("button", { name: "Hide " + TEST_WIDGET_NAME })).toBeVisible();
},
);

test("should move widget to top when 'show' button is clicked", async ({ homeserver, page, user }) => {
await makeRoomWithWidgetAndGoTo(homeserver, user, page);

await page.getByRole("button", { name: "Show " + TEST_WIDGET_NAME }).click();
await expect(page.getByRole("button", { name: "Hide " + TEST_WIDGET_NAME })).toBeVisible();

await expect(page.locator('iframe[title="Name of the test widget"]')).toBeVisible();
});

test("should move widget to left when 'hide' button is clicked", async ({ homeserver, page, user }) => {
const roomId = await makeRoomWithWidgetAndGoTo(homeserver, user, page);
await moveWidgetToContainer(homeserver, user, roomId, "top");

await expect(page.locator('iframe[title="Name of the test widget"]')).toBeVisible();

await page.getByRole("button", { name: "Hide " + TEST_WIDGET_NAME }).click();

await expect(page.locator('iframe[title="Name of the test widget"]')).not.toBeVisible();
});

test("uses widget icon for button image if present", async ({ homeserver, page, user }) => {
await makeRoomWithWidgetAndGoTo(homeserver, user, page, "mxc://fakehomeserver/fake_content_id");

await expect(
page.getByRole("button", { name: "Show " + TEST_WIDGET_NAME }).getByRole("img"),
).toHaveAttribute("src", /\/_matrix\/media\/v3\/download\/fakehomeserver\/fake_content_id$/);
});

test(
"uses built in icon for widgets with no avatar",
{ tag: ["@screenshot"] },
async ({ homeserver, page, user }) => {
await makeRoomWithWidgetAndGoTo(homeserver, user, page);

await expect(page.getByRole("button", { name: "Show " + TEST_WIDGET_NAME })).toMatchScreenshot(
"widget-toggle-button-default-icon.png",
);
},
);
});
});
28 changes: 28 additions & 0 deletions modules/widget-toggles/element-web/external_all.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
diff --git a/modules/widget-toggles/element-web/vite.config.ts b/modules/widget-toggles/element-web/vite.config.ts
index b65e3ec..9d1ba49 100644
--- a/modules/widget-toggles/element-web/vite.config.ts
+++ b/modules/widget-toggles/element-web/vite.config.ts
@@ -28,7 +28,14 @@ export default defineConfig({
target: "esnext",
sourcemap: true,
rollupOptions: {
- external: ["react"],
+ // make sure to externalize deps that shouldn't be bundled
+ // into your library
+ external: [
+ "react",
+ "react-dom",
+ "@vector-im/compound-design-tokens",
+ "@vector-im/compound-web"
+ ],
},
},
plugins: [
@@ -41,6 +48,7 @@ export default defineConfig({
externalGlobals({
// Reuse React from the host app
react: "window.React",
+ "@vector-im/compound-web": "window.CompoundWeb",
}),
],
define: {
41 changes: 41 additions & 0 deletions modules/widget-toggles/element-web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@element-hq/element-web-module-widget-toggle",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "lib/index.js",
"license": "SEE LICENSE IN README.md",
"scripts": {
"prepare": "vite build",
"lint:types": "tsc --noEmit",
"lint:codestyle": "echo 'handled by lint:eslint'",
"test": "vitest run --coverage"
},
"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",
"typescript": "^5.7.3",
"vite": "^7.1.11",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-svgr": "^4.3.0",
"vitest": "^4.0.0",
"vitest-sonar-reporter": "^2.0.0"
},
"dependencies": {
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.0.0",
"matrix-widget-api": "^1.17.0",
"styled-components": "^6.3.11",
"zod": "^4.3.6"
}
}
27 changes: 27 additions & 0 deletions modules/widget-toggles/element-web/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
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 { z, type input } from "zod/mini";

z.config(z.locales.en());

export const WidgetTogglesConfig = z.object({
/**
* The widget types to show a toggle for.
*/
types: z.array(z.string()),
});

export type WidgetTogglesConfig = z.infer<typeof WidgetTogglesConfig>;

export const CONFIG_KEY = "io.element.element-web-modules.widget-toggles";

declare module "@element-hq/element-web-module-api" {
export interface Config {
[CONFIG_KEY]: input<WidgetTogglesConfig>;
}
}
Loading
Loading