diff --git a/modules/widget-toggles/element-web/e2e/fixture/widget.html b/modules/widget-toggles/element-web/e2e/fixture/widget.html
new file mode 100644
index 0000000000..c2c28e414e
--- /dev/null
+++ b/modules/widget-toggles/element-web/e2e/fixture/widget.html
@@ -0,0 +1,5 @@
+
+
+ This is the content of the widget
+
+
diff --git a/modules/widget-toggles/element-web/e2e/snapshots/widget-toggles.spec.ts/widget-toggle-button-default-icon-linux.png b/modules/widget-toggles/element-web/e2e/snapshots/widget-toggles.spec.ts/widget-toggle-button-default-icon-linux.png
new file mode 100644
index 0000000000..a171d0adc8
Binary files /dev/null and b/modules/widget-toggles/element-web/e2e/snapshots/widget-toggles.spec.ts/widget-toggle-button-default-icon-linux.png differ
diff --git a/modules/widget-toggles/element-web/e2e/widget-toggles.spec.ts b/modules/widget-toggles/element-web/e2e/widget-toggles.spec.ts
new file mode 100644
index 0000000000..556408b56f
--- /dev/null
+++ b/modules/widget-toggles/element-web/e2e/widget-toggles.spec.ts
@@ -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;
+}>({
+ navigationJsonResolver: async ({}, use) => {
+ await use(Promise.withResolvers());
+ },
+});
+
+const TEST_WIDGET_NAME = "Name of the test widget";
+
+async function makeRoomWithWidgetAndGoTo(
+ homeserver: StartedHomeserverContainer,
+ user: Credentials,
+ page: Page,
+ avatarUrl?: string,
+): Promise {
+ 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 {
+ 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",
+ );
+ },
+ );
+ });
+});