diff --git a/cypress/e2e/integration-manager/get-openid-token.spec.ts b/cypress/e2e/integration-manager/get-openid-token.spec.ts
deleted file mode 100644
index b2dcb9146a..0000000000
--- a/cypress/e2e/integration-manager/get-openid-token.spec.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/// <reference types="cypress" />
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { UserCredentials } from "../../support/login";
-
-const ROOM_NAME = "Integration Manager Test";
-const USER_DISPLAY_NAME = "Alice";
-
-const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
-const INTEGRATION_MANAGER_HTML = `
-    <html lang="en">
-        <head>
-            <title>Fake Integration Manager</title>
-        </head>
-        <body>
-            <button name="Send" id="send-action">Press to send action</button>
-            <button name="Close" id="close">Press to close</button>
-            <p id="message-response">No response</p>
-            <script>
-                document.getElementById("send-action").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "get_open_id_token",
-                        },
-                        '*',
-                    );
-                };
-                document.getElementById("close").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "close_scalar",
-                        },
-                        '*',
-                    );
-                };
-                // Listen for a postmessage response
-                window.addEventListener("message", (event) => {
-                    document.getElementById("message-response").innerText = JSON.stringify(event.data);
-                });
-            </script>
-        </body>
-    </html>
-`;
-
-function openIntegrationManager() {
-    cy.findByRole("button", { name: "Room info" }).click();
-    cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
-}
-
-function sendActionFromIntegrationManager(integrationManagerUrl: string) {
-    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-        cy.findByRole("button", { name: "Press to send action" }).should("exist").click();
-    });
-}
-
-describe("Integration Manager: Get OpenID Token", () => {
-    let testUser: UserCredentials;
-    let homeserver: HomeserverInstance;
-    let integrationManagerUrl: string;
-
-    beforeEach(() => {
-        cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
-            integrationManagerUrl = url;
-        });
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
-                cy.window().then((win) => {
-                    win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
-                    win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
-                });
-            }).then((user) => {
-                testUser = user;
-            });
-
-            cy.setAccountData("m.widgets", {
-                "m.integration_manager": {
-                    content: {
-                        type: "m.integration_manager",
-                        name: "Integration Manager",
-                        url: integrationManagerUrl,
-                        data: {
-                            api_url: integrationManagerUrl,
-                        },
-                    },
-                    id: "integration-manager",
-                },
-            }).as("integrationManager");
-
-            // Succeed when checking the token is valid
-            cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
-                req.continue((res) => {
-                    return res.send(200, {
-                        user_id: testUser.userId,
-                    });
-                });
-            });
-
-            cy.createRoom({
-                name: ROOM_NAME,
-            }).as("roomId");
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should successfully obtain an openID token", () => {
-        cy.all([cy.get<{}>("@integrationManager")]).then(() => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            openIntegrationManager();
-            sendActionFromIntegrationManager(integrationManagerUrl);
-
-            cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                cy.get("#message-response").within(() => {
-                    cy.findByText(/access_token/);
-                });
-            });
-        });
-    });
-});
diff --git a/cypress/e2e/integration-manager/kick.spec.ts b/cypress/e2e/integration-manager/kick.spec.ts
deleted file mode 100644
index 7075c1c199..0000000000
--- a/cypress/e2e/integration-manager/kick.spec.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/// <reference types="cypress" />
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { MatrixClient } from "../../global";
-import { UserCredentials } from "../../support/login";
-
-const ROOM_NAME = "Integration Manager Test";
-const USER_DISPLAY_NAME = "Alice";
-const BOT_DISPLAY_NAME = "Bob";
-const KICK_REASON = "Goodbye";
-
-const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
-const INTEGRATION_MANAGER_HTML = `
-    <html lang="en">
-        <head>
-            <title>Fake Integration Manager</title>
-        </head>
-        <body>
-            <input type="text" id="target-room-id"/>
-            <input type="text" id="target-user-id"/>
-            <button name="Send" id="send-action">Press to send action</button>
-            <button name="Close" id="close">Press to close</button>
-            <script>
-                document.getElementById("send-action").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "kick",
-                            room_id: document.getElementById("target-room-id").value,
-                            user_id: document.getElementById("target-user-id").value,
-                            reason: "${KICK_REASON}",
-                        },
-                        '*',
-                    );
-                };
-                document.getElementById("close").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "close_scalar",
-                        },
-                        '*',
-                    );
-                };
-            </script>
-        </body>
-    </html>
-`;
-
-function openIntegrationManager() {
-    cy.findByRole("button", { name: "Room info" }).click();
-    cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
-}
-
-function closeIntegrationManager(integrationManagerUrl: string) {
-    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-        cy.findByRole("button", { name: "Press to close" }).should("exist").click();
-    });
-}
-
-function sendActionFromIntegrationManager(integrationManagerUrl: string, targetRoomId: string, targetUserId: string) {
-    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-        cy.get("#target-room-id").should("exist").type(targetRoomId);
-        cy.get("#target-user-id").should("exist").type(targetUserId);
-        cy.findByRole("button", { name: "Press to send action" }).should("exist").click();
-    });
-}
-
-function clickUntilGone(selector: string, attempt = 0) {
-    if (attempt === 11) {
-        throw new Error("clickUntilGone attempt count exceeded");
-    }
-
-    cy.get(selector)
-        .last()
-        .click()
-        .then(($button) => {
-            const exists = Cypress.$(selector).length > 0;
-            if (exists) {
-                clickUntilGone(selector, ++attempt);
-            }
-        });
-}
-
-function expectKickedMessage(shouldExist: boolean) {
-    // Expand any event summaries, we can't use a click multiple here because clicking one might de-render others
-    // This is quite horrible but seems the most stable way of clicking 0-N buttons,
-    // one at a time with a full re-evaluation after each click
-    clickUntilGone(".mx_GenericEventListSummary_toggle[aria-expanded=false]");
-
-    // Check for the event message (or lack thereof)
-    cy.findByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should(
-        shouldExist ? "exist" : "not.exist",
-    );
-}
-
-describe("Integration Manager: Kick", () => {
-    let testUser: UserCredentials;
-    let homeserver: HomeserverInstance;
-    let integrationManagerUrl: string;
-
-    beforeEach(() => {
-        cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
-            integrationManagerUrl = url;
-        });
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
-                cy.window().then((win) => {
-                    win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
-                    win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
-                });
-            }).then((user) => {
-                testUser = user;
-            });
-
-            cy.setAccountData("m.widgets", {
-                "m.integration_manager": {
-                    content: {
-                        type: "m.integration_manager",
-                        name: "Integration Manager",
-                        url: integrationManagerUrl,
-                        data: {
-                            api_url: integrationManagerUrl,
-                        },
-                    },
-                    id: "integration-manager",
-                },
-            }).as("integrationManager");
-
-            // Succeed when checking the token is valid
-            cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
-                req.continue((res) => {
-                    return res.send(200, {
-                        user_id: testUser.userId,
-                    });
-                });
-            });
-
-            cy.createRoom({
-                name: ROOM_NAME,
-            }).as("roomId");
-
-            cy.getBot(homeserver, { displayName: BOT_DISPLAY_NAME, autoAcceptInvites: true }).as("bob");
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should kick the target", () => {
-        cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
-            ([targetUser, roomId]) => {
-                const targetUserId = targetUser.getUserId();
-                cy.viewRoomByName(ROOM_NAME);
-                cy.inviteUser(roomId, targetUserId);
-                cy.findByText(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
-
-                openIntegrationManager();
-                sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
-                closeIntegrationManager(integrationManagerUrl);
-                expectKickedMessage(true);
-            },
-        );
-    });
-
-    it("should not kick the target if lacking permissions", () => {
-        cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
-            ([targetUser, roomId]) => {
-                const targetUserId = targetUser.getUserId();
-                cy.viewRoomByName(ROOM_NAME);
-                cy.inviteUser(roomId, targetUserId);
-                cy.findByText(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
-                cy.getClient()
-                    .then(async (client) => {
-                        await client.sendStateEvent(roomId, "m.room.power_levels", {
-                            kick: 50,
-                            users: {
-                                [testUser.userId]: 0,
-                            },
-                        });
-                    })
-                    .then(() => {
-                        openIntegrationManager();
-                        sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
-                        closeIntegrationManager(integrationManagerUrl);
-                        expectKickedMessage(false);
-                    });
-            },
-        );
-    });
-
-    it("should no-op if the target already left", () => {
-        cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
-            ([targetUser, roomId]) => {
-                const targetUserId = targetUser.getUserId();
-                cy.viewRoomByName(ROOM_NAME);
-                cy.inviteUser(roomId, targetUserId);
-                cy.findByText(`${BOT_DISPLAY_NAME} joined the room`)
-                    .should("exist")
-                    .then(async () => {
-                        await targetUser.leave(roomId);
-                    })
-                    .then(() => {
-                        openIntegrationManager();
-                        sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
-                        closeIntegrationManager(integrationManagerUrl);
-                        expectKickedMessage(false);
-                    });
-            },
-        );
-    });
-
-    it("should no-op if the target was banned", () => {
-        cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
-            ([targetUser, roomId]) => {
-                const targetUserId = targetUser.getUserId();
-                cy.viewRoomByName(ROOM_NAME);
-                cy.inviteUser(roomId, targetUserId);
-                cy.findByText(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
-                cy.getClient()
-                    .then(async (client) => {
-                        await client.ban(roomId, targetUserId);
-                    })
-                    .then(() => {
-                        openIntegrationManager();
-                        sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
-                        closeIntegrationManager(integrationManagerUrl);
-                        expectKickedMessage(false);
-                    });
-            },
-        );
-    });
-
-    it("should no-op if the target was never a room member", () => {
-        cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
-            ([targetUser, roomId]) => {
-                const targetUserId = targetUser.getUserId();
-                cy.viewRoomByName(ROOM_NAME);
-
-                openIntegrationManager();
-                sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
-                closeIntegrationManager(integrationManagerUrl);
-                expectKickedMessage(false);
-            },
-        );
-    });
-});
diff --git a/cypress/e2e/integration-manager/read_events.spec.ts b/cypress/e2e/integration-manager/read_events.spec.ts
deleted file mode 100644
index 65b195a3c7..0000000000
--- a/cypress/e2e/integration-manager/read_events.spec.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/// <reference types="cypress" />
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { UserCredentials } from "../../support/login";
-
-const ROOM_NAME = "Integration Manager Test";
-const USER_DISPLAY_NAME = "Alice";
-
-const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
-const INTEGRATION_MANAGER_HTML = `
-    <html lang="en">
-        <head>
-            <title>Fake Integration Manager</title>
-        </head>
-        <body>
-            <input type="text" id="target-room-id"/>
-            <input type="text" id="event-type"/>
-            <input type="text" id="state-key"/>
-            <button name="Send" id="send-action">Press to send action</button>
-            <button name="Close" id="close">Press to close</button>
-            <p id="message-response">No response</p>
-            <script>
-                document.getElementById("send-action").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "read_events",
-                            room_id: document.getElementById("target-room-id").value,
-                            type: document.getElementById("event-type").value,
-                            state_key: JSON.parse(document.getElementById("state-key").value),
-                        },
-                        '*',
-                    );
-                };
-                document.getElementById("close").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "close_scalar",
-                        },
-                        '*',
-                    );
-                };
-                // Listen for a postmessage response
-                window.addEventListener("message", (event) => {
-                    document.getElementById("message-response").innerText = JSON.stringify(event.data);
-                });
-            </script>
-        </body>
-    </html>
-`;
-
-function openIntegrationManager() {
-    cy.findByRole("button", { name: "Room info" }).click();
-    cy.get(".mx_RoomSummaryCard_appsGroup").within(() => {
-        cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
-    });
-}
-
-function sendActionFromIntegrationManager(
-    integrationManagerUrl: string,
-    targetRoomId: string,
-    eventType: string,
-    stateKey: string | boolean,
-) {
-    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-        cy.get("#target-room-id").should("exist").type(targetRoomId);
-        cy.get("#event-type").should("exist").type(eventType);
-        cy.get("#state-key").should("exist").type(JSON.stringify(stateKey));
-        cy.get("#send-action").should("exist").click();
-    });
-}
-
-describe("Integration Manager: Read Events", () => {
-    let testUser: UserCredentials;
-    let homeserver: HomeserverInstance;
-    let integrationManagerUrl: string;
-
-    beforeEach(() => {
-        cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
-            integrationManagerUrl = url;
-        });
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
-                cy.window().then((win) => {
-                    win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
-                    win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
-                });
-            }).then((user) => {
-                testUser = user;
-            });
-
-            cy.setAccountData("m.widgets", {
-                "m.integration_manager": {
-                    content: {
-                        type: "m.integration_manager",
-                        name: "Integration Manager",
-                        url: integrationManagerUrl,
-                        data: {
-                            api_url: integrationManagerUrl,
-                        },
-                    },
-                    id: "integration-manager",
-                },
-            }).as("integrationManager");
-
-            // Succeed when checking the token is valid
-            cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
-                req.continue((res) => {
-                    return res.send(200, {
-                        user_id: testUser.userId,
-                    });
-                });
-            });
-
-            cy.createRoom({
-                name: ROOM_NAME,
-            }).as("roomId");
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should read a state event by state key", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            const eventType = "io.element.integrations.installations";
-            const eventContent = {
-                foo: "bar",
-            };
-            const stateKey = "state-key-123";
-
-            // Send a state event
-            cy.getClient()
-                .then(async (client) => {
-                    return await client.sendStateEvent(roomId, eventType, eventContent, stateKey);
-                })
-                .then((event) => {
-                    openIntegrationManager();
-
-                    // Read state events
-                    sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey);
-
-                    // Check the response
-                    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                        cy.get("#message-response")
-                            .should("include.text", event.event_id)
-                            .should("include.text", `"content":${JSON.stringify(eventContent)}`);
-                    });
-                });
-        });
-    });
-
-    it("should read a state event with empty state key", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            const eventType = "io.element.integrations.installations";
-            const eventContent = {
-                foo: "bar",
-            };
-            const stateKey = "";
-
-            // Send a state event
-            cy.getClient()
-                .then(async (client) => {
-                    return await client.sendStateEvent(roomId, eventType, eventContent, stateKey);
-                })
-                .then((event) => {
-                    openIntegrationManager();
-
-                    // Read state events
-                    sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey);
-
-                    // Check the response
-                    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                        cy.get("#message-response")
-                            .should("include.text", event.event_id)
-                            .should("include.text", `"content":${JSON.stringify(eventContent)}`);
-                    });
-                });
-        });
-    });
-
-    it("should read state events with any state key", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            const eventType = "io.element.integrations.installations";
-
-            const stateKey1 = "state-key-123";
-            const eventContent1 = {
-                foo1: "bar1",
-            };
-            const stateKey2 = "state-key-456";
-            const eventContent2 = {
-                foo2: "bar2",
-            };
-            const stateKey3 = "state-key-789";
-            const eventContent3 = {
-                foo3: "bar3",
-            };
-
-            // Send state events
-            cy.getClient()
-                .then(async (client) => {
-                    return Promise.all([
-                        client.sendStateEvent(roomId, eventType, eventContent1, stateKey1),
-                        client.sendStateEvent(roomId, eventType, eventContent2, stateKey2),
-                        client.sendStateEvent(roomId, eventType, eventContent3, stateKey3),
-                    ]);
-                })
-                .then((events) => {
-                    openIntegrationManager();
-
-                    // Read state events
-                    sendActionFromIntegrationManager(
-                        integrationManagerUrl,
-                        roomId,
-                        eventType,
-                        true, // Any state key
-                    );
-
-                    // Check the response
-                    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                        cy.get("#message-response")
-                            .should("include.text", events[0].event_id)
-                            .should("include.text", `"content":${JSON.stringify(eventContent1)}`)
-                            .should("include.text", events[1].event_id)
-                            .should("include.text", `"content":${JSON.stringify(eventContent2)}`)
-                            .should("include.text", events[2].event_id)
-                            .should("include.text", `"content":${JSON.stringify(eventContent3)}`);
-                    });
-                });
-        });
-    });
-
-    it("should fail to read an event type which is not allowed", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            const eventType = "com.example.event";
-            const stateKey = "";
-
-            openIntegrationManager();
-
-            // Read state events
-            sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey);
-
-            // Check the response
-            cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                cy.get("#message-response").should("include.text", "Failed to read events");
-            });
-        });
-    });
-});
diff --git a/cypress/e2e/integration-manager/send_event.spec.ts b/cypress/e2e/integration-manager/send_event.spec.ts
deleted file mode 100644
index d8a746b423..0000000000
--- a/cypress/e2e/integration-manager/send_event.spec.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/// <reference types="cypress" />
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { UserCredentials } from "../../support/login";
-
-const ROOM_NAME = "Integration Manager Test";
-const USER_DISPLAY_NAME = "Alice";
-
-const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
-const INTEGRATION_MANAGER_HTML = `
-    <html lang="en">
-        <head>
-            <title>Fake Integration Manager</title>
-        </head>
-        <body>
-            <input type="text" id="target-room-id"/>
-            <input type="text" id="event-type"/>
-            <input type="text" id="state-key"/>
-            <input type="text" id="event-content"/>
-            <button name="Send" id="send-action">Press to send action</button>
-            <button name="Close" id="close">Press to close</button>
-            <p id="message-response">No response</p>
-            <script>
-                document.getElementById("send-action").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "send_event",
-                            room_id: document.getElementById("target-room-id").value,
-                            type: document.getElementById("event-type").value,
-                            state_key: document.getElementById("state-key").value,
-                            content: JSON.parse(document.getElementById("event-content").value),
-                        },
-                        '*',
-                    );
-                };
-                document.getElementById("close").onclick = () => {
-                    window.parent.postMessage(
-                        {
-                            action: "close_scalar",
-                        },
-                        '*',
-                    );
-                };
-                // Listen for a postmessage response
-                window.addEventListener("message", (event) => {
-                    document.getElementById("message-response").innerText = JSON.stringify(event.data);
-                });
-            </script>
-        </body>
-    </html>
-`;
-
-function openIntegrationManager() {
-    cy.findByRole("button", { name: "Room info" }).click();
-    cy.get(".mx_RoomSummaryCard_appsGroup").within(() => {
-        cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
-    });
-}
-
-function sendActionFromIntegrationManager(
-    integrationManagerUrl: string,
-    targetRoomId: string,
-    eventType: string,
-    stateKey: string,
-    content: Record<string, unknown>,
-) {
-    cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-        cy.get("#target-room-id").should("exist").type(targetRoomId);
-        cy.get("#event-type").should("exist").type(eventType);
-        if (stateKey) {
-            cy.get("#state-key").should("exist").type(stateKey);
-        }
-        cy.get("#event-content").should("exist").type(JSON.stringify(content), { parseSpecialCharSequences: false });
-        cy.get("#send-action").should("exist").click();
-    });
-}
-
-describe("Integration Manager: Send Event", () => {
-    let testUser: UserCredentials;
-    let homeserver: HomeserverInstance;
-    let integrationManagerUrl: string;
-
-    beforeEach(() => {
-        cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
-            integrationManagerUrl = url;
-        });
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
-                cy.window().then((win) => {
-                    win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
-                    win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
-                });
-            }).then((user) => {
-                testUser = user;
-            });
-
-            cy.setAccountData("m.widgets", {
-                "m.integration_manager": {
-                    content: {
-                        type: "m.integration_manager",
-                        name: "Integration Manager",
-                        url: integrationManagerUrl,
-                        data: {
-                            api_url: integrationManagerUrl,
-                        },
-                    },
-                    id: "integration-manager",
-                },
-            }).as("integrationManager");
-
-            // Succeed when checking the token is valid
-            cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
-                req.continue((res) => {
-                    return res.send(200, {
-                        user_id: testUser.userId,
-                    });
-                });
-            });
-
-            cy.createRoom({
-                name: ROOM_NAME,
-            }).as("roomId");
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should send a state event", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            openIntegrationManager();
-
-            const eventType = "io.element.integrations.installations";
-            const eventContent = {
-                foo: "bar",
-            };
-            const stateKey = "state-key-123";
-
-            // Send the event
-            sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
-
-            // Check the response
-            cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                cy.get("#message-response").should("include.text", "event_id");
-            });
-
-            // Check the event
-            cy.getClient()
-                .then(async (client) => {
-                    return await client.getStateEvent(roomId, eventType, stateKey);
-                })
-                .then((event) => {
-                    expect(event).to.deep.equal(eventContent);
-                });
-        });
-    });
-
-    it("should send a state event with empty content", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            openIntegrationManager();
-
-            const eventType = "io.element.integrations.installations";
-            const eventContent = {};
-            const stateKey = "state-key-123";
-
-            // Send the event
-            sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
-
-            // Check the response
-            cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                cy.get("#message-response").should("include.text", "event_id");
-            });
-
-            // Check the event
-            cy.getClient()
-                .then(async (client) => {
-                    return await client.getStateEvent(roomId, eventType, stateKey);
-                })
-                .then((event) => {
-                    expect(event).to.be.empty;
-                });
-        });
-    });
-
-    it("should send a state event with empty state key", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            openIntegrationManager();
-
-            const eventType = "io.element.integrations.installations";
-            const eventContent = {
-                foo: "bar",
-            };
-            const stateKey = "";
-
-            // Send the event
-            sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
-
-            // Check the response
-            cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                cy.get("#message-response").should("include.text", "event_id");
-            });
-
-            // Check the event
-            cy.getClient()
-                .then(async (client) => {
-                    return await client.getStateEvent(roomId, eventType, stateKey);
-                })
-                .then((event) => {
-                    expect(event).to.deep.equal(eventContent);
-                });
-        });
-    });
-
-    it("should fail to send an event type which is not allowed", () => {
-        cy.all([cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(([roomId]) => {
-            cy.viewRoomByName(ROOM_NAME);
-
-            openIntegrationManager();
-
-            const eventType = "com.example.event";
-            const eventContent = {
-                foo: "bar",
-            };
-            const stateKey = "";
-
-            // Send the event
-            sendActionFromIntegrationManager(integrationManagerUrl, roomId, eventType, stateKey, eventContent);
-
-            // Check the response
-            cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
-                cy.get("#message-response").should("include.text", "Failed to send event");
-            });
-        });
-    });
-});
diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts
deleted file mode 100644
index 58e4c09679..0000000000
--- a/cypress/e2e/widgets/events.spec.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
-Copyright 2022 Mikhail Aheichyk
-Copyright 2022 Nordeck IT + Consulting GmbH.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/// <reference types="cypress" />
-
-import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
-
-import type { MatrixClient } from "matrix-js-sdk/src/matrix";
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { UserCredentials } from "../../support/login";
-import { waitForRoom } from "../utils";
-
-const DEMO_WIDGET_ID = "demo-widget-id";
-const DEMO_WIDGET_NAME = "Demo Widget";
-const DEMO_WIDGET_TYPE = "demo";
-const ROOM_NAME = "Demo";
-
-const DEMO_WIDGET_HTML = `
-    <html lang="en">
-        <head>
-            <title>Demo Widget</title>
-            <script>
-                let sendEventCount = 0
-                window.onmessage = ev => {
-                    if (ev.data.action === 'capabilities') {
-                        window.parent.postMessage(Object.assign({
-                            response: {
-                                capabilities: [
-                                    "org.matrix.msc2762.timeline:*",
-                                    "org.matrix.msc2762.receive.state_event:m.room.topic",
-                                    "org.matrix.msc2762.send.event:net.widget_echo"
-                                ]
-                            },
-                        }, ev.data), '*');
-                    } else if (ev.data.action === 'send_event' && !ev.data.response) {
-                        // wraps received event into 'net.widget_echo' and sends back
-                        sendEventCount += 1
-                        window.parent.postMessage({
-                            api: "fromWidget",
-                            widgetId: ev.data.widgetId,
-                            requestId: 'widget-' + sendEventCount,
-                            action: "send_event",
-                            data: {
-                                type: 'net.widget_echo',
-                                content: ev.data.data // sets matrix event to the content returned
-                            },
-                        }, '*')
-                    }
-                };
-            </script>
-        </head>
-        <body>
-            <button id="demo">Demo</button>
-        </body>
-    </html>
-`;
-
-describe("Widget Events", () => {
-    let homeserver: HomeserverInstance;
-    let user: UserCredentials;
-    let bot: MatrixClient;
-    let demoWidgetUrl: string;
-
-    beforeEach(() => {
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, "Mike").then((_user) => {
-                user = _user;
-            });
-            cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => {
-                bot = _bot;
-            });
-        });
-        cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => {
-            demoWidgetUrl = url;
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should be updated if user is re-invited into the room with updated state event", () => {
-        cy.createRoom({
-            name: ROOM_NAME,
-            invite: [bot.getUserId()],
-        }).then((roomId) => {
-            // setup widget via state event
-            cy.getClient()
-                .then(async (matrixClient) => {
-                    const content: IWidget = {
-                        id: DEMO_WIDGET_ID,
-                        creatorUserId: "somebody",
-                        type: DEMO_WIDGET_TYPE,
-                        name: DEMO_WIDGET_NAME,
-                        url: demoWidgetUrl,
-                    };
-                    await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID);
-                })
-                .as("widgetEventSent");
-
-            // set initial layout
-            cy.getClient()
-                .then(async (matrixClient) => {
-                    const content = {
-                        widgets: {
-                            [DEMO_WIDGET_ID]: {
-                                container: "top",
-                                index: 1,
-                                width: 100,
-                                height: 0,
-                            },
-                        },
-                    };
-                    await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
-                })
-                .as("layoutEventSent");
-
-            // open the room
-            cy.viewRoomByName(ROOM_NAME);
-
-            // approve capabilities
-            cy.get(".mx_WidgetCapabilitiesPromptDialog").within(() => {
-                cy.findByRole("button", { name: "Approve" }).click();
-            });
-
-            cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(async () => {
-                // bot creates a new room with 'm.room.topic'
-                const { room_id: roomNew } = await bot.createRoom({
-                    name: "New room",
-                    initial_state: [
-                        {
-                            type: "m.room.topic",
-                            state_key: "",
-                            content: {
-                                topic: "topic initial",
-                            },
-                        },
-                    ],
-                });
-
-                await bot.invite(roomNew, user.userId);
-
-                // widget should receive 'm.room.topic' event after invite
-                cy.window().then(async (win) => {
-                    await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
-                        const events = room.getLiveTimeline().getEvents();
-                        return events.some(
-                            (e) =>
-                                e.getType() === "net.widget_echo" &&
-                                e.getContent().type === "m.room.topic" &&
-                                e.getContent().content.topic === "topic initial",
-                        );
-                    });
-                });
-
-                // update the topic
-                await bot.sendStateEvent(
-                    roomNew,
-                    "m.room.topic",
-                    {
-                        topic: "topic updated",
-                    },
-                    "",
-                );
-
-                await bot.invite(roomNew, user.userId, "something changed in the room");
-
-                // widget should receive updated 'm.room.topic' event after re-invite
-                cy.window().then(async (win) => {
-                    await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
-                        const events = room.getLiveTimeline().getEvents();
-                        return events.some(
-                            (e) =>
-                                e.getType() === "net.widget_echo" &&
-                                e.getContent().type === "m.room.topic" &&
-                                e.getContent().content.topic === "topic updated",
-                        );
-                    });
-                });
-            });
-        });
-    });
-});
diff --git a/cypress/e2e/widgets/layout.spec.ts b/cypress/e2e/widgets/layout.spec.ts
deleted file mode 100644
index 16470fd5a0..0000000000
--- a/cypress/e2e/widgets/layout.spec.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
-Copyright 2022 Oliver Sand
-Copyright 2022 Nordeck IT + Consulting GmbH.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { IWidget } from "matrix-widget-api";
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-
-const ROOM_NAME = "Test Room";
-const WIDGET_ID = "fake-widget";
-const WIDGET_HTML = `
-    <html lang="en">
-        <head>
-            <title>Fake Widget</title>
-        </head>
-        <body>
-            Hello World
-        </body>
-    </html>
-`;
-
-describe("Widget Layout", () => {
-    let widgetUrl: string;
-    let homeserver: HomeserverInstance;
-    let roomId: string;
-
-    beforeEach(() => {
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, "Sally");
-        });
-        cy.serveHtmlFile(WIDGET_HTML).then((url) => {
-            widgetUrl = url;
-        });
-
-        cy.createRoom({
-            name: ROOM_NAME,
-        }).then((id) => {
-            roomId = id;
-
-            // setup widget via state event
-            cy.getClient()
-                .then(async (matrixClient) => {
-                    const content: IWidget = {
-                        id: WIDGET_ID,
-                        creatorUserId: "somebody",
-                        type: "widget",
-                        name: "widget",
-                        url: widgetUrl,
-                    };
-                    await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
-                })
-                .as("widgetEventSent");
-
-            // set initial layout
-            cy.getClient()
-                .then(async (matrixClient) => {
-                    const content = {
-                        widgets: {
-                            [WIDGET_ID]: {
-                                container: "top",
-                                index: 1,
-                                width: 100,
-                                height: 0,
-                            },
-                        },
-                    };
-                    await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
-                })
-                .as("layoutEventSent");
-        });
-
-        cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(() => {
-            // open the room
-            cy.viewRoomByName(ROOM_NAME);
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should be set properly", () => {
-        cy.get(".mx_AppsDrawer").percySnapshotElement("Widgets drawer on the timeline (AppsDrawer)");
-    });
-
-    it("manually resize the height of the top container layout", () => {
-        cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
-
-        cy.get(".mx_AppsDrawer_resizer_container_handle")
-            .trigger("mousedown")
-            .trigger("mousemove", { clientX: 0, clientY: 550, force: true })
-            .trigger("mouseup", { clientX: 0, clientY: 550, force: true });
-
-        cy.get('iframe[title="widget"]').invoke("height").should("be.greaterThan", 400);
-    });
-
-    it("programatically resize the height of the top container layout", () => {
-        cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
-
-        cy.getClient().then(async (matrixClient) => {
-            const content = {
-                widgets: {
-                    [WIDGET_ID]: {
-                        container: "top",
-                        index: 1,
-                        width: 100,
-                        height: 100,
-                    },
-                },
-            };
-            await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
-        });
-
-        cy.get('iframe[title="widget"]').invoke("height").should("be.greaterThan", 400);
-    });
-});
diff --git a/cypress/e2e/widgets/widget-pip-close.spec.ts b/cypress/e2e/widgets/widget-pip-close.spec.ts
deleted file mode 100644
index ca717947d0..0000000000
--- a/cypress/e2e/widgets/widget-pip-close.spec.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
-Copyright 2022 Mikhail Aheichyk
-Copyright 2022 Nordeck IT + Consulting GmbH.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/// <reference types="cypress" />
-
-import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
-
-import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { UserCredentials } from "../../support/login";
-
-const DEMO_WIDGET_ID = "demo-widget-id";
-const DEMO_WIDGET_NAME = "Demo Widget";
-const DEMO_WIDGET_TYPE = "demo";
-const ROOM_NAME = "Demo";
-
-const DEMO_WIDGET_HTML = `
-    <html lang="en">
-        <head>
-            <title>Demo Widget</title>
-            <script>
-                window.onmessage = ev => {
-                    if (ev.data.action === 'capabilities') {
-                        window.parent.postMessage(Object.assign({
-                            response: {
-                                capabilities: []
-                            },
-                        }, ev.data), '*');
-                    }
-                };
-            </script>
-        </head>
-        <body>
-            <button id="demo">Demo</button>
-        </body>
-    </html>
-`;
-
-// mostly copied from src/utils/WidgetUtils.waitForRoomWidget with small modifications
-function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: string, add: boolean): Promise<void> {
-    const matrixClient = win.mxMatrixClientPeg.get();
-
-    return new Promise((resolve, reject) => {
-        function eventsInIntendedState(evList) {
-            const widgetPresent = evList.some((ev) => {
-                return ev.getContent() && ev.getContent()["id"] === widgetId;
-            });
-            if (add) {
-                return widgetPresent;
-            } else {
-                return !widgetPresent;
-            }
-        }
-
-        const room = matrixClient.getRoom(roomId);
-
-        const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
-        if (eventsInIntendedState(startingWidgetEvents)) {
-            resolve();
-            return;
-        }
-
-        function onRoomStateEvents(ev: MatrixEvent) {
-            if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return;
-
-            const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
-
-            if (eventsInIntendedState(currentWidgetEvents)) {
-                matrixClient.removeListener(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
-                resolve();
-            }
-        }
-
-        matrixClient.on(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
-    });
-}
-
-describe("Widget PIP", () => {
-    let homeserver: HomeserverInstance;
-    let user: UserCredentials;
-    let bot: MatrixClient;
-    let demoWidgetUrl: string;
-
-    function roomCreateAddWidgetPip(userRemove: "leave" | "kick" | "ban") {
-        cy.createRoom({
-            name: ROOM_NAME,
-            invite: [bot.getUserId()],
-        }).then((roomId) => {
-            // sets bot to Admin and user to Moderator
-            cy.getClient()
-                .then((matrixClient) => {
-                    return matrixClient.sendStateEvent(roomId, "m.room.power_levels", {
-                        users: {
-                            [user.userId]: 50,
-                            [bot.getUserId()]: 100,
-                        },
-                    });
-                })
-                .as("powerLevelsChanged");
-
-            // bot joins the room
-            cy.botJoinRoom(bot, roomId).as("botJoined");
-
-            // setup widget via state event
-            cy.getClient()
-                .then(async (matrixClient) => {
-                    const content: IWidget = {
-                        id: DEMO_WIDGET_ID,
-                        creatorUserId: "somebody",
-                        type: DEMO_WIDGET_TYPE,
-                        name: DEMO_WIDGET_NAME,
-                        url: demoWidgetUrl,
-                    };
-                    await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID);
-                })
-                .as("widgetEventSent");
-
-            // open the room
-            cy.viewRoomByName(ROOM_NAME);
-
-            cy.all([
-                cy.get<string>("@powerLevelsChanged"),
-                cy.get<string>("@botJoined"),
-                cy.get<string>("@widgetEventSent"),
-            ]).then(() => {
-                cy.window().then(async (win) => {
-                    // wait for widget state event
-                    await waitForRoomWidget(win, DEMO_WIDGET_ID, roomId, true);
-
-                    // activate widget in pip mode
-                    win.mxActiveWidgetStore.setWidgetPersistence(DEMO_WIDGET_ID, roomId, true);
-
-                    // checks that pip window is opened
-                    cy.get(".mx_WidgetPip").should("exist");
-
-                    // checks that widget is opened in pip
-                    cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => {
-                        cy.get("#demo")
-                            .should("exist")
-                            .then(async () => {
-                                const userId = user.userId;
-                                if (userRemove == "leave") {
-                                    cy.getClient().then(async (matrixClient) => {
-                                        await matrixClient.leave(roomId);
-                                    });
-                                } else if (userRemove == "kick") {
-                                    await bot.kick(roomId, userId);
-                                } else if (userRemove == "ban") {
-                                    await bot.ban(roomId, userId);
-                                }
-
-                                // checks that pip window is closed
-                                cy.get(".mx_WidgetPip").should("not.exist");
-                            });
-                    });
-                });
-            });
-        });
-    }
-
-    beforeEach(() => {
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, "Mike").then((_user) => {
-                user = _user;
-            });
-            cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: false }).then((_bot) => {
-                bot = _bot;
-            });
-        });
-        cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => {
-            demoWidgetUrl = url;
-        });
-    });
-
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
-
-    it("should be closed on leave", () => {
-        roomCreateAddWidgetPip("leave");
-    });
-
-    it("should be closed on kick", () => {
-        roomCreateAddWidgetPip("kick");
-    });
-
-    it("should be closed on ban", () => {
-        roomCreateAddWidgetPip("ban");
-    });
-});
diff --git a/playwright/e2e/integration-manager/get-openid-token.spec.ts b/playwright/e2e/integration-manager/get-openid-token.spec.ts
new file mode 100644
index 0000000000..c107bb2cbc
--- /dev/null
+++ b/playwright/e2e/integration-manager/get-openid-token.spec.ts
@@ -0,0 +1,128 @@
+/*
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import { openIntegrationManager } from "./utils";
+
+const ROOM_NAME = "Integration Manager Test";
+
+const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
+const INTEGRATION_MANAGER_HTML = `
+    <html lang="en">
+        <head>
+            <title>Fake Integration Manager</title>
+        </head>
+        <body>
+            <button name="Send" id="send-action">Press to send action</button>
+            <button name="Close" id="close">Press to close</button>
+            <p id="message-response">No response</p>
+            <script>
+                document.getElementById("send-action").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "get_open_id_token",
+                        },
+                        '*',
+                    );
+                };
+                document.getElementById("close").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "close_scalar",
+                        },
+                        '*',
+                    );
+                };
+                // Listen for a postmessage response
+                window.addEventListener("message", (event) => {
+                    document.getElementById("message-response").innerText = JSON.stringify(event.data);
+                });
+            </script>
+        </body>
+    </html>
+`;
+
+async function sendActionFromIntegrationManager(page: Page, integrationManagerUrl: string) {
+    const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+    await iframe.getByRole("button", { name: "Press to send action" }).click();
+}
+
+test.describe("Integration Manager: Get OpenID Token", () => {
+    test.use({
+        displayName: "Alice",
+        room: async ({ user, app }, use) => {
+            const roomId = await app.client.createRoom({
+                name: ROOM_NAME,
+            });
+            await use({ roomId });
+        },
+    });
+
+    let integrationManagerUrl: string;
+    test.beforeEach(async ({ page, webserver }) => {
+        integrationManagerUrl = webserver.start(INTEGRATION_MANAGER_HTML);
+
+        await page.addInitScript(
+            ({ token, integrationManagerUrl }) => {
+                window.localStorage.setItem("mx_scalar_token", token);
+                window.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, token);
+            },
+            {
+                token: INTEGRATION_MANAGER_TOKEN,
+                integrationManagerUrl,
+            },
+        );
+    });
+
+    test.beforeEach(async ({ page, user, app, room }) => {
+        await app.client.setAccountData("m.widgets", {
+            "m.integration_manager": {
+                content: {
+                    type: "m.integration_manager",
+                    name: "Integration Manager",
+                    url: integrationManagerUrl,
+                    data: {
+                        api_url: integrationManagerUrl,
+                    },
+                },
+                id: "integration-manager",
+            },
+        });
+
+        // Succeed when checking the token is valid
+        await page.route(
+            `${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`,
+            async (route) => {
+                await route.fulfill({
+                    json: {
+                        user_id: user.userId,
+                    },
+                });
+            },
+        );
+
+        await app.viewRoomByName(ROOM_NAME);
+    });
+
+    test("should successfully obtain an openID token", async ({ page }) => {
+        await openIntegrationManager(page);
+        await sendActionFromIntegrationManager(page, integrationManagerUrl);
+
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response").getByText(/access_token/)).toBeVisible();
+    });
+});
diff --git a/playwright/e2e/integration-manager/kick.spec.ts b/playwright/e2e/integration-manager/kick.spec.ts
new file mode 100644
index 0000000000..b5ca6a1b3a
--- /dev/null
+++ b/playwright/e2e/integration-manager/kick.spec.ts
@@ -0,0 +1,226 @@
+/*
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import { openIntegrationManager } from "./utils";
+
+const ROOM_NAME = "Integration Manager Test";
+const USER_DISPLAY_NAME = "Alice";
+const BOT_DISPLAY_NAME = "Bob";
+const KICK_REASON = "Goodbye";
+
+const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
+const INTEGRATION_MANAGER_HTML = `
+    <html lang="en">
+        <head>
+            <title>Fake Integration Manager</title>
+        </head>
+        <body>
+            <input type="text" id="target-room-id"/>
+            <input type="text" id="target-user-id"/>
+            <button name="Send" id="send-action">Press to send action</button>
+            <button name="Close" id="close">Press to close</button>
+            <script>
+                document.getElementById("send-action").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "kick",
+                            room_id: document.getElementById("target-room-id").value,
+                            user_id: document.getElementById("target-user-id").value,
+                            reason: "${KICK_REASON}",
+                        },
+                        '*',
+                    );
+                };
+                document.getElementById("close").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "close_scalar",
+                        },
+                        '*',
+                    );
+                };
+            </script>
+        </body>
+    </html>
+`;
+
+async function closeIntegrationManager(page: Page, integrationManagerUrl: string) {
+    const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+    await iframe.getByRole("button", { name: "Press to close" }).click();
+}
+
+async function sendActionFromIntegrationManager(
+    page: Page,
+    integrationManagerUrl: string,
+    targetRoomId: string,
+    targetUserId: string,
+) {
+    const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+    await iframe.locator("#target-room-id").fill(targetRoomId);
+    await iframe.locator("#target-user-id").fill(targetUserId);
+    await iframe.getByRole("button", { name: "Press to send action" }).click();
+}
+
+async function clickUntilGone(page: Page, selector: string, attempt = 0) {
+    if (attempt === 11) {
+        throw new Error("clickUntilGone attempt count exceeded");
+    }
+
+    await page.locator(selector).last().click();
+
+    const count = await page.locator(selector).count();
+    if (count > 0) {
+        return clickUntilGone(page, selector, ++attempt);
+    }
+}
+
+async function expectKickedMessage(page: Page, shouldExist: boolean) {
+    // Expand any event summaries, we can't use a click multiple here because clicking one might de-render others
+    // This is quite horrible but seems the most stable way of clicking 0-N buttons,
+    // one at a time with a full re-evaluation after each click
+    await clickUntilGone(page, ".mx_GenericEventListSummary_toggle[aria-expanded=false]");
+
+    // Check for the event message (or lack thereof)
+    await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
+        visible: shouldExist,
+    });
+}
+
+test.describe("Integration Manager: Kick", () => {
+    test.use({
+        displayName: "Alice",
+        room: async ({ user, app }, use) => {
+            const roomId = await app.client.createRoom({
+                name: ROOM_NAME,
+            });
+            await use({ roomId });
+        },
+        botCreateOpts: {
+            displayName: BOT_DISPLAY_NAME,
+            autoAcceptInvites: true,
+        },
+    });
+
+    let integrationManagerUrl: string;
+    test.beforeEach(async ({ page, webserver }) => {
+        integrationManagerUrl = webserver.start(INTEGRATION_MANAGER_HTML);
+
+        await page.addInitScript(
+            ({ token, integrationManagerUrl }) => {
+                window.localStorage.setItem("mx_scalar_token", token);
+                window.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, token);
+            },
+            {
+                token: INTEGRATION_MANAGER_TOKEN,
+                integrationManagerUrl,
+            },
+        );
+    });
+
+    test.beforeEach(async ({ page, user, app, room }) => {
+        await app.client.setAccountData("m.widgets", {
+            "m.integration_manager": {
+                content: {
+                    type: "m.integration_manager",
+                    name: "Integration Manager",
+                    url: integrationManagerUrl,
+                    data: {
+                        api_url: integrationManagerUrl,
+                    },
+                },
+                id: "integration-manager",
+            },
+        });
+
+        // Succeed when checking the token is valid
+        await page.route(
+            `${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`,
+            async (route) => {
+                await route.fulfill({
+                    json: {
+                        user_id: user.userId,
+                    },
+                });
+            },
+        );
+
+        await app.viewRoomByName(ROOM_NAME);
+    });
+
+    test("should kick the target", async ({ page, app, bot: targetUser, room }) => {
+        await app.viewRoomByName(ROOM_NAME);
+        await app.client.inviteUser(room.roomId, targetUser.credentials.userId);
+        await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible();
+
+        await openIntegrationManager(page);
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId);
+        await closeIntegrationManager(page, integrationManagerUrl);
+        await expectKickedMessage(page, true);
+    });
+
+    test("should not kick the target if lacking permissions", async ({ page, app, user, bot: targetUser, room }) => {
+        await app.viewRoomByName(ROOM_NAME);
+        await app.client.inviteUser(room.roomId, targetUser.credentials.userId);
+        await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible();
+
+        await app.client.sendStateEvent(room.roomId, "m.room.power_levels", {
+            kick: 50,
+            users: {
+                [user.userId]: 0,
+            },
+        });
+
+        await openIntegrationManager(page);
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId);
+        await closeIntegrationManager(page, integrationManagerUrl);
+        await expectKickedMessage(page, false);
+    });
+
+    test("should no-op if the target already left", async ({ page, app, bot: targetUser, room }) => {
+        await app.viewRoomByName(ROOM_NAME);
+        await app.client.inviteUser(room.roomId, targetUser.credentials.userId);
+        await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible();
+        await targetUser.leave(room.roomId);
+
+        await openIntegrationManager(page);
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId);
+        await closeIntegrationManager(page, integrationManagerUrl);
+        await expectKickedMessage(page, false);
+    });
+
+    test("should no-op if the target was banned", async ({ page, app, bot: targetUser, room }) => {
+        await app.viewRoomByName(ROOM_NAME);
+        await app.client.inviteUser(room.roomId, targetUser.credentials.userId);
+        await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible();
+        await app.client.ban(room.roomId, targetUser.credentials.userId);
+
+        await openIntegrationManager(page);
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId);
+        await closeIntegrationManager(page, integrationManagerUrl);
+        await expectKickedMessage(page, false);
+    });
+
+    test("should no-op if the target was never a room member", async ({ page, app, bot: targetUser, room }) => {
+        await app.viewRoomByName(ROOM_NAME);
+
+        await openIntegrationManager(page);
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId);
+        await closeIntegrationManager(page, integrationManagerUrl);
+        await expectKickedMessage(page, false);
+    });
+});
diff --git a/playwright/e2e/integration-manager/read_events.spec.ts b/playwright/e2e/integration-manager/read_events.spec.ts
new file mode 100644
index 0000000000..b178596674
--- /dev/null
+++ b/playwright/e2e/integration-manager/read_events.spec.ts
@@ -0,0 +1,233 @@
+/*
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import { openIntegrationManager } from "./utils";
+
+const ROOM_NAME = "Integration Manager Test";
+
+const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
+const INTEGRATION_MANAGER_HTML = `
+    <html lang="en">
+        <head>
+            <title>Fake Integration Manager</title>
+        </head>
+        <body>
+            <input type="text" id="target-room-id"/>
+            <input type="text" id="event-type"/>
+            <input type="text" id="state-key"/>
+            <button name="Send" id="send-action">Press to send action</button>
+            <button name="Close" id="close">Press to close</button>
+            <p id="message-response">No response</p>
+            <script>
+                document.getElementById("send-action").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "read_events",
+                            room_id: document.getElementById("target-room-id").value,
+                            type: document.getElementById("event-type").value,
+                            state_key: JSON.parse(document.getElementById("state-key").value),
+                        },
+                        '*',
+                    );
+                };
+                document.getElementById("close").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "close_scalar",
+                        },
+                        '*',
+                    );
+                };
+                // Listen for a postmessage response
+                window.addEventListener("message", (event) => {
+                    document.getElementById("message-response").innerText = JSON.stringify(event.data);
+                });
+            </script>
+        </body>
+    </html>
+`;
+
+async function sendActionFromIntegrationManager(
+    page: Page,
+    integrationManagerUrl: string,
+    targetRoomId: string,
+    eventType: string,
+    stateKey: string | boolean,
+) {
+    const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+    await iframe.locator("#target-room-id").fill(targetRoomId);
+    await iframe.locator("#event-type").fill(eventType);
+    await iframe.locator("#state-key").fill(JSON.stringify(stateKey));
+    await iframe.locator("#send-action").click();
+}
+
+test.describe("Integration Manager: Read Events", () => {
+    test.use({
+        displayName: "Alice",
+        room: async ({ user, app }, use) => {
+            const roomId = await app.client.createRoom({
+                name: ROOM_NAME,
+            });
+            await use({ roomId });
+        },
+    });
+
+    let integrationManagerUrl: string;
+    test.beforeEach(async ({ page, webserver }) => {
+        integrationManagerUrl = webserver.start(INTEGRATION_MANAGER_HTML);
+
+        await page.addInitScript(
+            ({ token, integrationManagerUrl }) => {
+                window.localStorage.setItem("mx_scalar_token", token);
+                window.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, token);
+            },
+            {
+                token: INTEGRATION_MANAGER_TOKEN,
+                integrationManagerUrl,
+            },
+        );
+    });
+
+    test.beforeEach(async ({ page, user, app, room }) => {
+        await app.client.setAccountData("m.widgets", {
+            "m.integration_manager": {
+                content: {
+                    type: "m.integration_manager",
+                    name: "Integration Manager",
+                    url: integrationManagerUrl,
+                    data: {
+                        api_url: integrationManagerUrl,
+                    },
+                },
+                id: "integration-manager",
+            },
+        });
+
+        // Succeed when checking the token is valid
+        await page.route(
+            `${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`,
+            async (route) => {
+                await route.fulfill({
+                    json: {
+                        user_id: user.userId,
+                    },
+                });
+            },
+        );
+
+        await app.viewRoomByName(ROOM_NAME);
+    });
+
+    test("should read a state event by state key", async ({ page, app, room }) => {
+        const eventType = "io.element.integrations.installations";
+        const eventContent = {
+            foo: "bar",
+        };
+        const stateKey = "state-key-123";
+
+        // Send a state event
+        const sendEventResponse = await app.client.sendStateEvent(room.roomId, eventType, eventContent, stateKey);
+        await openIntegrationManager(page);
+
+        // Read state events
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, eventType, stateKey);
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText(sendEventResponse.event_id);
+        await expect(iframe.locator("#message-response")).toContainText(`"content":${JSON.stringify(eventContent)}`);
+    });
+
+    test("should read a state event with empty state key", async ({ page, app, room }) => {
+        const eventType = "io.element.integrations.installations";
+        const eventContent = {
+            foo: "bar",
+        };
+        const stateKey = "";
+
+        // Send a state event
+        const sendEventResponse = await app.client.sendStateEvent(room.roomId, eventType, eventContent, stateKey);
+        await openIntegrationManager(page);
+
+        // Read state events
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, eventType, stateKey);
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText(sendEventResponse.event_id);
+        await expect(iframe.locator("#message-response")).toContainText(`"content":${JSON.stringify(eventContent)}`);
+    });
+
+    test("should read state events with any state key", async ({ page, app, room }) => {
+        const eventType = "io.element.integrations.installations";
+
+        const stateKey1 = "state-key-123";
+        const eventContent1 = {
+            foo1: "bar1",
+        };
+        const stateKey2 = "state-key-456";
+        const eventContent2 = {
+            foo2: "bar2",
+        };
+        const stateKey3 = "state-key-789";
+        const eventContent3 = {
+            foo3: "bar3",
+        };
+
+        // Send state events
+        const sendEventResponses = await Promise.all([
+            app.client.sendStateEvent(room.roomId, eventType, eventContent1, stateKey1),
+            app.client.sendStateEvent(room.roomId, eventType, eventContent2, stateKey2),
+            app.client.sendStateEvent(room.roomId, eventType, eventContent3, stateKey3),
+        ]);
+
+        await openIntegrationManager(page);
+
+        // Read state events
+        await sendActionFromIntegrationManager(
+            page,
+            integrationManagerUrl,
+            room.roomId,
+            eventType,
+            true, // Any state key
+        );
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText(sendEventResponses[0].event_id);
+        await expect(iframe.locator("#message-response")).toContainText(`"content":${JSON.stringify(eventContent1)}`);
+        await expect(iframe.locator("#message-response")).toContainText(sendEventResponses[1].event_id);
+        await expect(iframe.locator("#message-response")).toContainText(`"content":${JSON.stringify(eventContent2)}`);
+        await expect(iframe.locator("#message-response")).toContainText(sendEventResponses[2].event_id);
+        await expect(iframe.locator("#message-response")).toContainText(`"content":${JSON.stringify(eventContent3)}`);
+    });
+
+    test("should fail to read an event type which is not allowed", async ({ page, room }) => {
+        const eventType = "com.example.event";
+        const stateKey = "";
+
+        await openIntegrationManager(page);
+
+        // Read state events
+        await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, eventType, stateKey);
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText("Failed to read events");
+    });
+});
diff --git a/playwright/e2e/integration-manager/send_event.spec.ts b/playwright/e2e/integration-manager/send_event.spec.ts
new file mode 100644
index 0000000000..61bad8a3ec
--- /dev/null
+++ b/playwright/e2e/integration-manager/send_event.spec.ts
@@ -0,0 +1,255 @@
+/*
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import { openIntegrationManager } from "./utils";
+
+const ROOM_NAME = "Integration Manager Test";
+
+const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
+const INTEGRATION_MANAGER_HTML = `
+    <html lang="en">
+        <head>
+            <title>Fake Integration Manager</title>
+        </head>
+        <body>
+            <input type="text" id="target-room-id"/>
+            <input type="text" id="event-type"/>
+            <input type="text" id="state-key"/>
+            <input type="text" id="event-content"/>
+            <button name="Send" id="send-action">Press to send action</button>
+            <button name="Close" id="close">Press to close</button>
+            <p id="message-response">No response</p>
+            <script>
+                document.getElementById("send-action").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "send_event",
+                            room_id: document.getElementById("target-room-id").value,
+                            type: document.getElementById("event-type").value,
+                            state_key: document.getElementById("state-key").value,
+                            content: JSON.parse(document.getElementById("event-content").value),
+                        },
+                        '*',
+                    );
+                };
+                document.getElementById("close").onclick = () => {
+                    window.parent.postMessage(
+                        {
+                            action: "close_scalar",
+                        },
+                        '*',
+                    );
+                };
+                // Listen for a postmessage response
+                window.addEventListener("message", (event) => {
+                    document.getElementById("message-response").innerText = JSON.stringify(event.data);
+                });
+            </script>
+        </body>
+    </html>
+`;
+
+async function sendActionFromIntegrationManager(
+    page: Page,
+    integrationManagerUrl: string,
+    targetRoomId: string,
+    eventType: string,
+    stateKey: string,
+    content: Record<string, unknown>,
+) {
+    const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+    await iframe.locator("#target-room-id").fill(targetRoomId);
+    await iframe.locator("#event-type").fill(eventType);
+    if (stateKey) {
+        await iframe.locator("#state-key").fill(stateKey);
+    }
+    await iframe.locator("#event-content").fill(JSON.stringify(content));
+    await iframe.locator("#send-action").click();
+}
+
+test.describe("Integration Manager: Send Event", () => {
+    test.use({
+        displayName: "Alice",
+        room: async ({ user, app }, use) => {
+            const roomId = await app.client.createRoom({
+                name: ROOM_NAME,
+            });
+            await use({ roomId });
+        },
+    });
+
+    let integrationManagerUrl: string;
+    test.beforeEach(async ({ page, webserver }) => {
+        integrationManagerUrl = webserver.start(INTEGRATION_MANAGER_HTML);
+
+        await page.addInitScript(
+            ({ token, integrationManagerUrl }) => {
+                window.localStorage.setItem("mx_scalar_token", token);
+                window.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, token);
+            },
+            {
+                token: INTEGRATION_MANAGER_TOKEN,
+                integrationManagerUrl,
+            },
+        );
+    });
+
+    test.beforeEach(async ({ page, user, app, room }) => {
+        await app.client.setAccountData("m.widgets", {
+            "m.integration_manager": {
+                content: {
+                    type: "m.integration_manager",
+                    name: "Integration Manager",
+                    url: integrationManagerUrl,
+                    data: {
+                        api_url: integrationManagerUrl,
+                    },
+                },
+                id: "integration-manager",
+            },
+        });
+
+        // Succeed when checking the token is valid
+        await page.route(
+            `${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`,
+            async (route) => {
+                await route.fulfill({
+                    json: {
+                        user_id: user.userId,
+                    },
+                });
+            },
+        );
+
+        await app.viewRoomByName(ROOM_NAME);
+        await openIntegrationManager(page);
+    });
+
+    test("should send a state event", async ({ page, app, room }) => {
+        const eventType = "io.element.integrations.installations";
+        const eventContent = {
+            foo: "bar",
+        };
+        const stateKey = "state-key-123";
+
+        // Send the event
+        await sendActionFromIntegrationManager(
+            page,
+            integrationManagerUrl,
+            room.roomId,
+            eventType,
+            stateKey,
+            eventContent,
+        );
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText("event_id");
+
+        // Check the event
+        const event = await app.client.evaluate(
+            (cli, { room, eventType, stateKey }) => {
+                return cli.getStateEvent(room.roomId, eventType, stateKey);
+            },
+            { room, eventType, stateKey },
+        );
+        expect(event).toMatchObject(eventContent);
+    });
+
+    test("should send a state event with empty content", async ({ page, app, room }) => {
+        const eventType = "io.element.integrations.installations";
+        const eventContent = {};
+        const stateKey = "state-key-123";
+
+        // Send the event
+        await sendActionFromIntegrationManager(
+            page,
+            integrationManagerUrl,
+            room.roomId,
+            eventType,
+            stateKey,
+            eventContent,
+        );
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText("event_id");
+
+        // Check the event
+        const event = await app.client.evaluate(
+            (cli, { room, eventType, stateKey }) => {
+                return cli.getStateEvent(room.roomId, eventType, stateKey);
+            },
+            { room, eventType, stateKey },
+        );
+        expect(event).toMatchObject({});
+    });
+
+    test("should send a state event with empty state key", async ({ page, app, room }) => {
+        const eventType = "io.element.integrations.installations";
+        const eventContent = {
+            foo: "bar",
+        };
+        const stateKey = "";
+
+        // Send the event
+        await sendActionFromIntegrationManager(
+            page,
+            integrationManagerUrl,
+            room.roomId,
+            eventType,
+            stateKey,
+            eventContent,
+        );
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText("event_id");
+
+        // Check the event
+        const event = await app.client.evaluate(
+            (cli, { room, eventType, stateKey }) => {
+                return cli.getStateEvent(room.roomId, eventType, stateKey);
+            },
+            { room, eventType, stateKey },
+        );
+        expect(event).toMatchObject(eventContent);
+    });
+
+    test("should fail to send an event type which is not allowed", async ({ page, room }) => {
+        const eventType = "com.example.event";
+        const eventContent = {
+            foo: "bar",
+        };
+        const stateKey = "";
+
+        // Send the event
+        await sendActionFromIntegrationManager(
+            page,
+            integrationManagerUrl,
+            room.roomId,
+            eventType,
+            stateKey,
+            eventContent,
+        );
+
+        // Check the response
+        const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`);
+        await expect(iframe.locator("#message-response")).toContainText("Failed to send event");
+    });
+});
diff --git a/playwright/e2e/integration-manager/utils.ts b/playwright/e2e/integration-manager/utils.ts
new file mode 100644
index 0000000000..259ff732c7
--- /dev/null
+++ b/playwright/e2e/integration-manager/utils.ts
@@ -0,0 +1,25 @@
+/*
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { Page } from "@playwright/test";
+
+export async function openIntegrationManager(page: Page) {
+    await page.getByRole("button", { name: "Room info" }).click();
+    await page
+        .locator(".mx_RoomSummaryCard_appsGroup")
+        .getByRole("button", { name: "Add widgets, bridges & bots" })
+        .click();
+}
diff --git a/playwright/e2e/widgets/events.spec.ts b/playwright/e2e/widgets/events.spec.ts
new file mode 100644
index 0000000000..a336bd2cfa
--- /dev/null
+++ b/playwright/e2e/widgets/events.spec.ts
@@ -0,0 +1,176 @@
+/*
+Copyright 2022 Mikhail Aheichyk
+Copyright 2022 Nordeck IT + Consulting GmbH.
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { test } from "../../element-web-test";
+import { waitForRoom } from "../utils";
+
+const DEMO_WIDGET_ID = "demo-widget-id";
+const DEMO_WIDGET_NAME = "Demo Widget";
+const DEMO_WIDGET_TYPE = "demo";
+const ROOM_NAME = "Demo";
+
+const DEMO_WIDGET_HTML = `
+    <html lang="en">
+        <head>
+            <title>Demo Widget</title>
+            <script>
+                let sendEventCount = 0
+                window.onmessage = ev => {
+                    if (ev.data.action === 'capabilities') {
+                        window.parent.postMessage(Object.assign({
+                            response: {
+                                capabilities: [
+                                    "org.matrix.msc2762.timeline:*",
+                                    "org.matrix.msc2762.receive.state_event:m.room.topic",
+                                    "org.matrix.msc2762.send.event:net.widget_echo"
+                                ]
+                            },
+                        }, ev.data), '*');
+                    } else if (ev.data.action === 'send_event' && !ev.data.response) {
+                        // wraps received event into 'net.widget_echo' and sends back
+                        sendEventCount += 1
+                        window.parent.postMessage({
+                            api: "fromWidget",
+                            widgetId: ev.data.widgetId,
+                            requestId: 'widget-' + sendEventCount,
+                            action: "send_event",
+                            data: {
+                                type: 'net.widget_echo',
+                                content: ev.data.data // sets matrix event to the content returned
+                            },
+                        }, '*')
+                    }
+                };
+            </script>
+        </head>
+        <body>
+            <button id="demo">Demo</button>
+        </body>
+    </html>
+`;
+
+test.describe("Widget Events", () => {
+    test.use({
+        displayName: "Mike",
+        botCreateOpts: { displayName: "Bot", autoAcceptInvites: true },
+    });
+
+    let demoWidgetUrl: string;
+    test.beforeEach(async ({ webserver }) => {
+        demoWidgetUrl = webserver.start(DEMO_WIDGET_HTML);
+    });
+
+    test("should be updated if user is re-invited into the room with updated state event", async ({
+        page,
+        app,
+        user,
+        bot,
+    }) => {
+        const roomId = await app.client.createRoom({
+            name: ROOM_NAME,
+            invite: [bot.credentials.userId],
+        });
+
+        // setup widget via state event
+        await app.client.sendStateEvent(
+            roomId,
+            "im.vector.modular.widgets",
+            {
+                id: DEMO_WIDGET_ID,
+                creatorUserId: "somebody",
+                type: DEMO_WIDGET_TYPE,
+                name: DEMO_WIDGET_NAME,
+                url: demoWidgetUrl,
+            },
+            DEMO_WIDGET_ID,
+        );
+
+        // set initial layout
+        await app.client.sendStateEvent(
+            roomId,
+            "io.element.widgets.layout",
+            {
+                widgets: {
+                    [DEMO_WIDGET_ID]: {
+                        container: "top",
+                        index: 1,
+                        width: 100,
+                        height: 0,
+                    },
+                },
+            },
+            "",
+        );
+
+        // open the room
+        await app.viewRoomByName(ROOM_NAME);
+
+        // approve capabilities
+        await page.locator(".mx_WidgetCapabilitiesPromptDialog").getByRole("button", { name: "Approve" }).click();
+
+        // bot creates a new room with 'm.room.topic'
+        const roomNew = await bot.createRoom({
+            name: "New room",
+            initial_state: [
+                {
+                    type: "m.room.topic",
+                    state_key: "",
+                    content: {
+                        topic: "topic initial",
+                    },
+                },
+            ],
+        });
+
+        await bot.inviteUser(roomNew, user.userId);
+
+        // widget should receive 'm.room.topic' event after invite
+        await waitForRoom(page, app.client, roomId, (room) => {
+            const events = room.getLiveTimeline().getEvents();
+            return events.some(
+                (e) =>
+                    e.getType() === "net.widget_echo" &&
+                    e.getContent().type === "m.room.topic" &&
+                    e.getContent().content.topic === "topic initial",
+            );
+        });
+
+        // update the topic
+        await bot.sendStateEvent(
+            roomNew,
+            "m.room.topic",
+            {
+                topic: "topic updated",
+            },
+            "",
+        );
+
+        await bot.inviteUser(roomNew, user.userId);
+
+        // widget should receive updated 'm.room.topic' event after re-invite
+        await waitForRoom(page, app.client, roomId, (room) => {
+            const events = room.getLiveTimeline().getEvents();
+            return events.some(
+                (e) =>
+                    e.getType() === "net.widget_echo" &&
+                    e.getContent().type === "m.room.topic" &&
+                    e.getContent().content.topic === "topic updated",
+            );
+        });
+    });
+});
diff --git a/playwright/e2e/widgets/layout.spec.ts b/playwright/e2e/widgets/layout.spec.ts
new file mode 100644
index 0000000000..a5dd856a93
--- /dev/null
+++ b/playwright/e2e/widgets/layout.spec.ts
@@ -0,0 +1,119 @@
+/*
+Copyright 2022 Oliver Sand
+Copyright 2022 Nordeck IT + Consulting GmbH.
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { test, expect } from "../../element-web-test";
+
+const ROOM_NAME = "Test Room";
+const WIDGET_ID = "fake-widget";
+const WIDGET_HTML = `
+    <html lang="en">
+        <head>
+            <title>Fake Widget</title>
+        </head>
+        <body>
+            Hello World
+        </body>
+    </html>
+`;
+
+test.describe("Widget Layout", () => {
+    test.use({
+        displayName: "Sally",
+    });
+
+    let roomId: string;
+    let widgetUrl: string;
+    test.beforeEach(async ({ webserver, app, user }) => {
+        widgetUrl = webserver.start(WIDGET_HTML);
+
+        roomId = await app.client.createRoom({ name: ROOM_NAME });
+
+        // setup widget via state event
+        await app.client.sendStateEvent(
+            roomId,
+            "im.vector.modular.widgets",
+            {
+                id: WIDGET_ID,
+                creatorUserId: "somebody",
+                type: "widget",
+                name: "widget",
+                url: widgetUrl,
+            },
+            WIDGET_ID,
+        );
+
+        // set initial layout
+        await app.client.sendStateEvent(
+            roomId,
+            "io.element.widgets.layout",
+            {
+                widgets: {
+                    [WIDGET_ID]: {
+                        container: "top",
+                        index: 1,
+                        width: 100,
+                        height: 0,
+                    },
+                },
+            },
+            "",
+        );
+
+        // open the room
+        await app.viewRoomByName(ROOM_NAME);
+    });
+
+    test("should be set properly", async ({ page }) => {
+        await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png");
+    });
+
+    test("manually resize the height of the top container layout", async ({ page }) => {
+        const iframe = page.locator('iframe[title="widget"]');
+        expect((await iframe.boundingBox()).height).toBeLessThan(250);
+
+        await page.locator(".mx_AppsDrawer_resizer_container_handle").hover();
+        await page.mouse.down();
+        await page.mouse.move(0, 550);
+        await page.mouse.up();
+
+        expect((await iframe.boundingBox()).height).toBeGreaterThan(400);
+    });
+
+    test("programmatically resize the height of the top container layout", async ({ page, app }) => {
+        const iframe = page.locator('iframe[title="widget"]');
+        expect((await iframe.boundingBox()).height).toBeLessThan(250);
+
+        await app.client.sendStateEvent(
+            roomId,
+            "io.element.widgets.layout",
+            {
+                widgets: {
+                    [WIDGET_ID]: {
+                        container: "top",
+                        index: 1,
+                        width: 100,
+                        height: 500,
+                    },
+                },
+            },
+            "",
+        );
+
+        await expect.poll(async () => (await iframe.boundingBox()).height).toBeGreaterThan(400);
+    });
+});
diff --git a/cypress/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts
similarity index 52%
rename from cypress/e2e/widgets/stickers.spec.ts
rename to playwright/e2e/widgets/stickers.spec.ts
index d3e08f8405..37aaea58ce 100644
--- a/cypress/e2e/widgets/stickers.spec.ts
+++ b/playwright/e2e/widgets/stickers.spec.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-/// <reference types="cypress" />
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import type { Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import { ElementAppPage } from "../../pages/ElementAppPage";
 
 const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
 const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
@@ -33,7 +33,7 @@ const STICKER_MESSAGE = JSON.stringify({
         content: {
             body: STICKER_NAME,
             msgtype: "m.sticker",
-            url: "mxc://somewhere",
+            url: "mxc://localhost/somewhere",
         },
     },
     requestId: "1",
@@ -66,108 +66,86 @@ const WIDGET_HTML = `
     </html>
 `;
 
-function openStickerPicker() {
-    cy.openMessageComposerOptions().findByRole("menuitem", { name: "Sticker" }).click();
+async function openStickerPicker(app: ElementAppPage) {
+    const options = await app.openMessageComposerOptions();
+    await options.getByRole("menuitem", { name: "Sticker" }).click();
 }
 
-function sendStickerFromPicker() {
-    // Note: Until https://github.com/cypress-io/cypress/issues/136 is fixed we will need
-    // to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can
-    // break into the iframe for us :(
-    cy.accessIframe(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`).within({}, () => {
-        cy.get("#sendsticker").should("exist").click();
-    });
+async function sendStickerFromPicker(page: Page) {
+    const iframe = page.frameLocator(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`);
+    await iframe.locator("#sendsticker").click();
 
     // Sticker picker should close itself after sending.
-    cy.get(".mx_AppTileFullWidth#stickers").should("not.exist");
+    await expect(page.locator(".mx_AppTileFullWidth#stickers")).not.toBeVisible();
 }
 
-function expectTimelineSticker(roomId: string) {
+async function expectTimelineSticker(page: Page, roomId: string) {
     // Make sure it's in the right room
-    cy.get(".mx_EventTile_sticker > a").should("have.attr", "href").and("include", `/${roomId}/`);
+    await expect(page.locator(".mx_EventTile_sticker > a")).toHaveAttribute("href", new RegExp(`/${roomId}/`));
 
     // Make sure the image points at the sticker image. We will briefly show it
     // using the thumbnail URL, but as soon as that fails, we will switch to the
     // download URL.
-    cy.get<HTMLImageElement>(`img[alt="${STICKER_NAME}"][src*="download/somewhere"]`).should("exist");
+    await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute(
+        "src",
+        new RegExp("/download/localhost/somewhere"),
+    );
 }
 
-describe("Stickers", () => {
+test.describe("Stickers", () => {
+    test.use({
+        displayName: "Sally",
+    });
+
     // We spin up a web server for the sticker picker so that we're not testing to see if
     // sysadmins can deploy sticker pickers on the same Element domain - we actually want
     // to make sure that cross-origin postMessage works properly. This makes it difficult
     // to write the test though, as we have to juggle iframe logistics.
     //
     // See sendStickerFromPicker() for more detail on iframe comms.
-
     let stickerPickerUrl: string;
-    let homeserver: HomeserverInstance;
-    let userId: string;
-
-    beforeEach(() => {
-        cy.startHomeserver("default").then((data) => {
-            homeserver = data;
-
-            cy.initTestUser(homeserver, "Sally").then((user) => (userId = user.userId));
-        });
-        cy.serveHtmlFile(WIDGET_HTML).then((url) => {
-            stickerPickerUrl = url;
-        });
+    test.beforeEach(async ({ webserver }) => {
+        stickerPickerUrl = webserver.start(WIDGET_HTML);
     });
 
-    afterEach(() => {
-        cy.stopHomeserver(homeserver);
-        cy.stopWebServers();
-    });
+    test("should send a sticker to multiple rooms", async ({ page, app, user }) => {
+        const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
+        const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 });
 
-    it("should send a sticker to multiple rooms", () => {
-        cy.createRoom({
-            name: ROOM_NAME_1,
-        }).as("roomId1");
-        cy.createRoom({
-            name: ROOM_NAME_2,
-        }).as("roomId2");
-        cy.setAccountData("m.widgets", {
+        await app.client.setAccountData("m.widgets", {
             [STICKER_PICKER_WIDGET_ID]: {
                 content: {
                     type: "m.stickerpicker",
                     name: STICKER_PICKER_WIDGET_NAME,
                     url: stickerPickerUrl,
-                    creatorUserId: userId,
+                    creatorUserId: user.userId,
                 },
-                sender: userId,
+                sender: user.userId,
                 state_key: STICKER_PICKER_WIDGET_ID,
                 type: "m.widget",
                 id: STICKER_PICKER_WIDGET_ID,
             },
-        }).as("stickers");
-
-        cy.all([
-            cy.get<string>("@roomId1"),
-            cy.get<string>("@roomId2"),
-            cy.get<{}>("@stickers"), // just want to wait for it to be set up
-        ]).then(([roomId1, roomId2]) => {
-            cy.viewRoomByName(ROOM_NAME_1);
-            cy.url().should("contain", `/#/room/${roomId1}`);
-            openStickerPicker();
-            sendStickerFromPicker();
-            expectTimelineSticker(roomId1);
-
-            // Ensure that when we switch to a different room that the sticker
-            // goes to the right place
-            cy.viewRoomByName(ROOM_NAME_2);
-            cy.url().should("contain", `/#/room/${roomId2}`);
-            openStickerPicker();
-            sendStickerFromPicker();
-            expectTimelineSticker(roomId2);
         });
+
+        await app.viewRoomByName(ROOM_NAME_1);
+        await expect(page).toHaveURL(`/#/room/${roomId1}`);
+        await openStickerPicker(app);
+        await sendStickerFromPicker(page);
+        await expectTimelineSticker(page, roomId1);
+
+        // Ensure that when we switch to a different room that the sticker
+        // goes to the right place
+        await app.viewRoomByName(ROOM_NAME_2);
+        await expect(page).toHaveURL(`/#/room/${roomId2}`);
+        await openStickerPicker(app);
+        await sendStickerFromPicker(page);
+        await expectTimelineSticker(page, roomId2);
     });
 
-    it("should handle a sticker picker widget missing creatorUserId", () => {
-        cy.createRoom({
-            name: ROOM_NAME_1,
-        }).as("roomId1");
-        cy.setAccountData("m.widgets", {
+    test("should handle a sticker picker widget missing creatorUserId", async ({ page, app, user }) => {
+        const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
+
+        await app.client.setAccountData("m.widgets", {
             [STICKER_PICKER_WIDGET_ID]: {
                 content: {
                     type: "m.stickerpicker",
@@ -175,19 +153,17 @@ describe("Stickers", () => {
                     url: stickerPickerUrl,
                     // No creatorUserId
                 },
-                sender: userId,
+                sender: user.userId,
                 state_key: STICKER_PICKER_WIDGET_ID,
                 type: "m.widget",
                 id: STICKER_PICKER_WIDGET_ID,
             },
-        }).as("stickers");
-
-        cy.all([cy.get<string>("@roomId1"), cy.get<{}>("@stickers")]).then(([roomId1]) => {
-            cy.viewRoomByName(ROOM_NAME_1);
-            cy.url().should("contain", `/#/room/${roomId1}`);
-            openStickerPicker();
-            sendStickerFromPicker();
-            expectTimelineSticker(roomId1);
         });
+
+        await app.viewRoomByName(ROOM_NAME_1);
+        await expect(page).toHaveURL(`/#/room/${roomId1}`);
+        await openStickerPicker(app);
+        await sendStickerFromPicker(page);
+        await expectTimelineSticker(page, roomId1);
     });
 });
diff --git a/playwright/e2e/widgets/widget-pip-close.spec.ts b/playwright/e2e/widgets/widget-pip-close.spec.ts
new file mode 100644
index 0000000000..c8073a3405
--- /dev/null
+++ b/playwright/e2e/widgets/widget-pip-close.spec.ts
@@ -0,0 +1,169 @@
+/*
+Copyright 2022 Mikhail Aheichyk
+Copyright 2022 Nordeck IT + Consulting GmbH.
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
+import type { MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
+import { test, expect } from "../../element-web-test";
+import { Client } from "../../pages/client";
+
+const DEMO_WIDGET_ID = "demo-widget-id";
+const DEMO_WIDGET_NAME = "Demo Widget";
+const DEMO_WIDGET_TYPE = "demo";
+const ROOM_NAME = "Demo";
+
+const DEMO_WIDGET_HTML = `
+    <html lang="en">
+        <head>
+            <title>Demo Widget</title>
+            <script>
+                window.onmessage = ev => {
+                    if (ev.data.action === 'capabilities') {
+                        window.parent.postMessage(Object.assign({
+                            response: {
+                                capabilities: []
+                            },
+                        }, ev.data), '*');
+                    }
+                };
+            </script>
+        </head>
+        <body>
+            <button id="demo">Demo</button>
+        </body>
+    </html>
+`;
+
+// mostly copied from src/utils/WidgetUtils.waitForRoomWidget with small modifications
+async function waitForRoomWidget(client: Client, widgetId: string, roomId: string, add: boolean): Promise<void> {
+    await client.evaluate(
+        (matrixClient, { widgetId, roomId, add }) => {
+            return new Promise<void>((resolve, reject) => {
+                function eventsInIntendedState(evList: MatrixEvent[]) {
+                    const widgetPresent = evList.some((ev) => {
+                        return ev.getContent() && ev.getContent()["id"] === widgetId;
+                    });
+                    if (add) {
+                        return widgetPresent;
+                    } else {
+                        return !widgetPresent;
+                    }
+                }
+
+                const room = matrixClient.getRoom(roomId);
+
+                const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
+                if (eventsInIntendedState(startingWidgetEvents)) {
+                    resolve();
+                    return;
+                }
+
+                function onRoomStateEvents(ev: MatrixEvent) {
+                    if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return;
+
+                    const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
+
+                    if (eventsInIntendedState(currentWidgetEvents)) {
+                        matrixClient.removeListener("RoomState.events" as RoomStateEvent.Events, onRoomStateEvents);
+                        resolve();
+                    }
+                }
+
+                matrixClient.on("RoomState.events" as RoomStateEvent.Events, onRoomStateEvents);
+            });
+        },
+        { widgetId, roomId, add },
+    );
+}
+
+test.describe("Widget PIP", () => {
+    test.use({
+        displayName: "Mike",
+        botCreateOpts: { displayName: "Bot", autoAcceptInvites: false },
+    });
+
+    let demoWidgetUrl: string;
+    test.beforeEach(async ({ webserver }) => {
+        demoWidgetUrl = webserver.start(DEMO_WIDGET_HTML);
+    });
+
+    for (const userRemove of ["leave", "kick", "ban"] as const) {
+        test(`should be closed on ${userRemove}`, async ({ page, app, bot, user }) => {
+            const roomId = await app.client.createRoom({
+                name: ROOM_NAME,
+                invite: [bot.credentials.userId],
+            });
+
+            // sets bot to Admin and user to Moderator
+            await app.client.sendStateEvent(roomId, "m.room.power_levels", {
+                users: {
+                    [user.userId]: 50,
+                    [bot.credentials.userId]: 100,
+                },
+            });
+
+            // bot joins the room
+            await bot.joinRoom(roomId);
+
+            // setup widget via state event
+            const content: IWidget = {
+                id: DEMO_WIDGET_ID,
+                creatorUserId: "somebody",
+                type: DEMO_WIDGET_TYPE,
+                name: DEMO_WIDGET_NAME,
+                url: demoWidgetUrl,
+            };
+            await app.client.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID);
+
+            // open the room
+            await app.viewRoomByName(ROOM_NAME);
+
+            // wait for widget state event
+            await waitForRoomWidget(app.client, DEMO_WIDGET_ID, roomId, true);
+
+            // activate widget in pip mode
+            await page.evaluate(
+                ({ widgetId, roomId }) => {
+                    window.mxActiveWidgetStore.setWidgetPersistence(widgetId, roomId, true);
+                },
+                {
+                    widgetId: DEMO_WIDGET_ID,
+                    roomId,
+                },
+            );
+
+            // checks that pip window is opened
+            await expect(page.locator(".mx_WidgetPip")).toBeVisible();
+
+            // checks that widget is opened in pip
+            const iframe = page.frameLocator(`iframe[title="${DEMO_WIDGET_NAME}"]`);
+            await expect(iframe.locator("#demo")).toBeVisible();
+
+            const userId = user.userId;
+            if (userRemove == "leave") {
+                await app.client.leave(roomId);
+            } else if (userRemove == "kick") {
+                await bot.kick(roomId, userId);
+            } else if (userRemove == "ban") {
+                await bot.ban(roomId, userId);
+            }
+
+            // checks that pip window is closed
+            await expect(iframe.locator(".mx_WidgetPip")).not.toBeVisible();
+        });
+    }
+});
diff --git a/playwright/global.d.ts b/playwright/global.d.ts
index c537d0a142..166bfbe993 100644
--- a/playwright/global.d.ts
+++ b/playwright/global.d.ts
@@ -25,6 +25,9 @@ declare global {
         mxSettingsStore: {
             setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void>;
         };
+        mxActiveWidgetStore: {
+            setWidgetPersistence(widgetId: string, roomId: string | null, val: boolean): void;
+        };
         matrixcs: typeof Matrix;
     }
 }
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index fcd0d86e02..6c56cb90d3 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -314,6 +314,54 @@ export class Client {
         }, credentials);
     }
 
+    /**
+     * Sets account data for the user.
+     * @param type The type of account data to set
+     * @param content The content to set
+     */
+    public async setAccountData(type: string, content: IContent): Promise<void> {
+        const client = await this.prepareClient();
+        return client.evaluate(
+            async (client, { type, content }) => {
+                await client.setAccountData(type, content);
+            },
+            { type, content },
+        );
+    }
+
+    /**
+     * Sends a state event into the room.
+     * @param roomId ID of the room to send the event into
+     * @param eventType type of event to send
+     * @param content the event content to send
+     * @param stateKey the state key to use
+     */
+    public async sendStateEvent(
+        roomId: string,
+        eventType: string,
+        content: IContent,
+        stateKey?: string,
+    ): Promise<ISendEventResponse> {
+        const client = await this.prepareClient();
+        return client.evaluate(
+            async (client, { roomId, eventType, content, stateKey }) => {
+                return client.sendStateEvent(roomId, eventType, content, stateKey);
+            },
+            { roomId, eventType, content, stateKey },
+        );
+    }
+
+    /**
+     * Leaves the given room.
+     * @param roomId ID of the room to leave
+     */
+    public async leave(roomId: string): Promise<void> {
+        const client = await this.prepareClient();
+        return client.evaluate(async (client, roomId) => {
+            await client.leave(roomId);
+        }, roomId);
+    }
+
     /**
      * Sets the directory visibility for a room.
      * @param roomId ID of the room to set the directory visibility for
diff --git a/playwright/plugins/webserver/index.ts b/playwright/plugins/webserver/index.ts
index 2fe083f179..1bc2cbfa42 100644
--- a/playwright/plugins/webserver/index.ts
+++ b/playwright/plugins/webserver/index.ts
@@ -33,7 +33,7 @@ export class Webserver {
 
         const address = this.server.address() as AddressInfo;
         console.log(`Started webserver at ${address.address}:${address.port}`);
-        return `http://localhost:${address.port}/`;
+        return `http://localhost:${address.port}`;
     }
 
     public stop(): void {
diff --git a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png
new file mode 100644
index 0000000000..20618f5d66
Binary files /dev/null and b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png differ