mirror of
https://github.com/vector-im/element-web.git
synced 2026-07-08 00:09:47 +00:00
244 lines
10 KiB
TypeScript
244 lines
10 KiB
TypeScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { type SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
|
|
import { rejectToastIfExists } from "@element-hq/element-web-playwright-common";
|
|
|
|
import { test, expect } from "../../playwright/element-web-test.ts";
|
|
|
|
declare module "@element-hq/element-web-module-api" {
|
|
interface Config {
|
|
"io.element.element-web-modules.widget-lifecycle": {
|
|
widget_permissions: {
|
|
[url: string]: {
|
|
preload_approved?: boolean;
|
|
identity_approved?: boolean;
|
|
capabilities_approved?: string[];
|
|
};
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
const WIDGET_URL = "http://localhost:8080/widget.html";
|
|
|
|
test.use({
|
|
displayName: "Timmy",
|
|
synapseConfig: async ({ synapseConfig, _homeserver: homeserver }, use) => {
|
|
(homeserver as SynapseContainer).withConfigField("listeners[0].resources[0].names", ["client", "openid"]);
|
|
await use(synapseConfig);
|
|
},
|
|
page: async ({ context, page, moduleDir }, use) => {
|
|
await context.route(`${WIDGET_URL}*`, async (route) => {
|
|
await route.fulfill({ path: `${moduleDir}/e2e/fixture/widget.html`, contentType: "text/html" });
|
|
});
|
|
|
|
await page.goto("/");
|
|
await use(page);
|
|
},
|
|
bypassCSP: true,
|
|
launchOptions: {
|
|
args: ["--disable-web-security"],
|
|
},
|
|
});
|
|
|
|
test.describe("Widget Lifecycle", () => {
|
|
test.describe("trusted widgets", () => {
|
|
// Configure the module to pre-approve the widget URL for preloading, identity tokens,
|
|
// and the m.room.topic state event capability.
|
|
test.use({
|
|
config: {
|
|
"io.element.element-web-modules.widget-lifecycle": {
|
|
widget_permissions: {
|
|
[`${WIDGET_URL}*`]: {
|
|
preload_approved: true,
|
|
identity_approved: true,
|
|
capabilities_approved: ["org.matrix.msc2762.receive.state_event:m.room.topic"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
test("auto-approves preload and identity", async ({ page, user, homeserver }, testInfo) => {
|
|
await rejectToastIfExists(page, "Verify this device");
|
|
|
|
// A bot creates a room with the widget pinned to the top panel, then invites the test user.
|
|
// Because the widget was added by a different user (the bot), Element would normally show a
|
|
// preload consent dialog before loading it — this test verifies that dialog is skipped.
|
|
const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot");
|
|
const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>(
|
|
"POST",
|
|
"/v3/createRoom",
|
|
bot.accessToken,
|
|
{
|
|
name: "Trusted Widget",
|
|
},
|
|
);
|
|
await homeserver.csApi.request<{ event_id: string }>(
|
|
"PUT",
|
|
`/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`,
|
|
bot.accessToken,
|
|
{
|
|
id: "1",
|
|
creatorUserId: bot.userId,
|
|
type: "custom",
|
|
name: "Trusted Widget",
|
|
url: `${WIDGET_URL}?hsUrl=${encodeURIComponent(homeserver.baseUrl)}&caps=org.matrix.msc2762.receive.state_event:m.room.topic`,
|
|
},
|
|
);
|
|
await homeserver.csApi.request(
|
|
"PUT",
|
|
`/v3/rooms/${encodeURIComponent(roomId)}/state/io.element.widgets.layout/`,
|
|
bot.accessToken,
|
|
{
|
|
widgets: {
|
|
"1": {
|
|
container: "top",
|
|
},
|
|
},
|
|
},
|
|
);
|
|
await homeserver.csApi.request("POST", `/v3/rooms/${encodeURIComponent(roomId)}/invite`, bot.accessToken, {
|
|
user_id: user.userId,
|
|
});
|
|
|
|
await page.getByText("Trusted Widget").click();
|
|
await page.getByRole("button", { name: "Accept" }).click();
|
|
|
|
// No preload dialog should appear — the widget loads immediately.
|
|
await expect(page.getByRole("button", { name: "Continue" })).not.toBeVisible();
|
|
|
|
// The widget greets the user by ID, proving the identity token was also auto-approved
|
|
// and passed to the widget without any consent prompts.
|
|
await expect(
|
|
page
|
|
.frameLocator('iframe[title="Trusted Widget"]')
|
|
.getByRole("heading", { name: `Hello ${user.userId}!` }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("prompts for capabilities not in the allowlist", async ({ page, user, homeserver }, testInfo) => {
|
|
await rejectToastIfExists(page, "Verify this device");
|
|
|
|
const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot");
|
|
const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>(
|
|
"POST",
|
|
"/v3/createRoom",
|
|
bot.accessToken,
|
|
{
|
|
name: "Capabilities Widget",
|
|
},
|
|
);
|
|
// The widget requests two capabilities: m.room.topic (in the allowlist) and m.room.name (not in the allowlist).
|
|
await homeserver.csApi.request<{ event_id: string }>(
|
|
"PUT",
|
|
`/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`,
|
|
bot.accessToken,
|
|
{
|
|
id: "1",
|
|
creatorUserId: bot.userId,
|
|
type: "custom",
|
|
name: "Capabilities Widget",
|
|
url: `${WIDGET_URL}?hsUrl=${encodeURIComponent(homeserver.baseUrl)}&caps=org.matrix.msc2762.receive.state_event:m.room.topic,org.matrix.msc2762.receive.state_event:m.room.name`,
|
|
},
|
|
);
|
|
await homeserver.csApi.request(
|
|
"PUT",
|
|
`/v3/rooms/${encodeURIComponent(roomId)}/state/io.element.widgets.layout/`,
|
|
bot.accessToken,
|
|
{
|
|
widgets: {
|
|
"1": {
|
|
container: "top",
|
|
},
|
|
},
|
|
},
|
|
);
|
|
await homeserver.csApi.request("POST", `/v3/rooms/${encodeURIComponent(roomId)}/invite`, bot.accessToken, {
|
|
user_id: user.userId,
|
|
});
|
|
|
|
await page.getByText("Capabilities Widget").click();
|
|
await page.getByRole("button", { name: "Accept" }).click();
|
|
|
|
// A capabilities approval dialog should appear since m.room.name was not pre-approved.
|
|
await expect(page.getByRole("button", { name: "Approve" })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("untrusted widgets", () => {
|
|
// No widget URLs are pre-approved, so all lifecycle prompts should be shown to the user.
|
|
test.use({
|
|
config: {
|
|
"io.element.element-web-modules.widget-lifecycle": {
|
|
widget_permissions: {},
|
|
},
|
|
},
|
|
});
|
|
|
|
test("shows preload, capabilities, and OpenID dialogs for untrusted widgets", async ({
|
|
page,
|
|
user,
|
|
homeserver,
|
|
}, testInfo) => {
|
|
await rejectToastIfExists(page, "Verify this device");
|
|
|
|
const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot");
|
|
const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>(
|
|
"POST",
|
|
"/v3/createRoom",
|
|
bot.accessToken,
|
|
{
|
|
name: "Untrusted Widget",
|
|
},
|
|
);
|
|
await homeserver.csApi.request<{ event_id: string }>(
|
|
"PUT",
|
|
`/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`,
|
|
bot.accessToken,
|
|
{
|
|
id: "1",
|
|
creatorUserId: bot.userId,
|
|
type: "custom",
|
|
name: "Untrusted Widget",
|
|
url: `${WIDGET_URL}?hsUrl=${encodeURIComponent(homeserver.baseUrl)}&caps=org.matrix.msc2762.receive.state_event:m.room.topic`,
|
|
},
|
|
);
|
|
await homeserver.csApi.request(
|
|
"PUT",
|
|
`/v3/rooms/${encodeURIComponent(roomId)}/state/io.element.widgets.layout/`,
|
|
bot.accessToken,
|
|
{
|
|
widgets: {
|
|
"1": {
|
|
container: "top",
|
|
},
|
|
},
|
|
},
|
|
);
|
|
await homeserver.csApi.request("POST", `/v3/rooms/${encodeURIComponent(roomId)}/invite`, bot.accessToken, {
|
|
user_id: user.userId,
|
|
});
|
|
|
|
await page.getByText("Untrusted Widget").click();
|
|
await page.getByRole("button", { name: "Accept" }).click();
|
|
|
|
// 1. Preload consent dialog — shown because the widget was added by another user (the bot).
|
|
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible();
|
|
await page.getByRole("button", { name: "Continue" }).click();
|
|
|
|
// 2. Capabilities dialog — the widget requests m.room.topic once it loads.
|
|
await expect(page.getByRole("button", { name: "Approve" })).toBeVisible();
|
|
await page.getByRole("button", { name: "Approve" }).click();
|
|
|
|
// 3. OpenID identity dialog — the widget requests an identity token after capabilities are granted.
|
|
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible();
|
|
});
|
|
});
|
|
});
|