mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/

mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts

And a couple of gitignore tweaks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-24 15:43:58 +00:00
parent e7509c92a1
commit 91a3cb03c1
3408 changed files with 28 additions and 32 deletions
@@ -0,0 +1,168 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
Copyright 2022 Mikhail Aheichyk
Copyright 2022 Nordeck IT + Consulting GmbH.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { 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",
);
});
});
});
@@ -0,0 +1,51 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test as base, expect } from "../../element-web-test";
import { Bot } from "../../pages/bot";
const ROOM_NAME = "Jitsi Room";
const test = base.extend<{
bot1: Bot;
bot2: Bot;
}>({
bot2: async ({ page, homeserver }, use, testInfo) => {
const bot = new Bot(page, homeserver, { displayName: `ByteBot_${testInfo.testId}` });
await bot.prepareClient(); // eagerly register the bot
await use(bot);
},
});
test.describe("Jitsi Calls", () => {
test.use({
displayName: "Jimmy",
botCreateOpts: { displayName: "Bot", autoAcceptInvites: false },
});
test("should be able to pop out a jitsi widget", async ({ page, app, bot, bot2, context }) => {
const roomId = await app.client.createRoom({
name: ROOM_NAME,
invite: [bot.credentials.userId, bot2.credentials.userId],
});
await bot.joinRoom(roomId);
await bot2.joinRoom(roomId);
// open the room
await app.viewRoomByName(ROOM_NAME);
await page.getByRole("button", { name: "Video call" }).click();
const pagePromise = context.waitForEvent("page");
await page.getByRole("button", { name: "Popout widget" }).click();
const newPage = await pagePromise;
await expect(newPage.getByRole("button", { name: "Join Conference" })).toBeVisible();
});
});
@@ -0,0 +1,111 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
Copyright 2022 Oliver Sand
Copyright 2022 Nordeck IT + Consulting GmbH.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { 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", { tag: "@screenshot" }, 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);
});
});
@@ -0,0 +1,98 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
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), '*');
}
};
</script>
</head>
</html>
`;
test.describe("Widget permissions dialog", () => {
test.use({
displayName: "Mike",
});
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",
{ tag: "@screenshot" },
async ({ page, app, user, axe }) => {
const roomId = await app.client.createRoom({
name: ROOM_NAME,
});
// 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);
axe.disableRules(["color-contrast", "nested-interactive"]); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(page.locator(".mx_WidgetCapabilitiesPromptDialog")).toMatchScreenshot(
"widget-capabilites-prompt.png",
);
},
);
});
@@ -0,0 +1,203 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import * as fs from "node:fs";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Credentials } from "../../plugins/homeserver";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
const STICKER_NAME = "Test Sticker";
const ROOM_NAME_1 = "Sticker Test";
const ROOM_NAME_2 = "Sticker Test Two";
const STICKER_IMAGE = fs.readFileSync("playwright/sample-files/riot.png");
function getStickerMessage(contentUri: string, mimetype: string): string {
return JSON.stringify({
action: "m.sticker",
api: "fromWidget",
data: {
name: "teststicker",
description: STICKER_NAME,
file: "test.png",
content: {
body: STICKER_NAME,
info: {
h: 480,
mimetype: mimetype,
size: 13818,
w: 480,
},
msgtype: "m.sticker",
url: contentUri,
},
},
requestId: "1",
widgetId: STICKER_PICKER_WIDGET_ID,
});
}
function getWidgetHtml(contentUri: string, mimetype: string) {
const stickerMessage = getStickerMessage(contentUri, mimetype);
return `
<html lang="en">
<head>
<title>Fake Sticker Picker</title>
<script>
window.onmessage = ev => {
if (ev.data.action === 'capabilities') {
window.parent.postMessage(Object.assign({
response: {
capabilities: ["m.sticker"]
},
}, ev.data), '*');
}
};
</script>
</head>
<body>
<button name="Send" id="sendsticker">Press for sticker</button>
<script>
document.getElementById('sendsticker').onclick = () => {
window.parent.postMessage(${stickerMessage}, '*')
};
</script>
</body>
</html>
`;
}
async function openStickerPicker(app: ElementAppPage) {
const options = await app.openMessageComposerOptions();
await options.getByRole("menuitem", { name: "Sticker" }).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.
await expect(page.locator(".mx_AppTileFullWidth#stickers")).not.toBeVisible();
}
async function expectTimelineSticker(page: Page, serverName: string, roomId: string, contentUri: string) {
const contentId = contentUri.split("/").slice(-1)[0];
// Make sure it's in the right room
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.
await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute(
"src",
new RegExp(`/${serverName}/${contentId}`),
);
}
async function expectFileTile(page: Page, roomId: string, contentUri: string) {
await expect(page.locator(".mx_MFileBody_info_filename")).toContainText(STICKER_NAME);
}
async function setWidgetAccountData(
app: ElementAppPage,
user: Credentials,
stickerPickerUrl: string,
provideCreatorUserId: boolean = true,
) {
await app.client.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: {
content: {
type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl,
creatorUserId: provideCreatorUserId ? user.userId : undefined,
},
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
} as unknown as UserWidget,
});
}
test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.use({
displayName: "Sally",
room: async ({ app }, use) => {
const roomId = await app.client.createRoom({ name: ROOM_NAME_1 });
await use({ roomId });
},
});
// 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;
test("should send a sticker to multiple rooms", async ({ webserver, page, app, user, room }) => {
const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 });
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" });
const widgetHtml = getWidgetHtml(contentUri, "image/png");
stickerPickerUrl = webserver.start(widgetHtml);
await setWidgetAccountData(app, user, stickerPickerUrl);
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app);
await sendStickerFromPicker(page);
await expectTimelineSticker(page, user.homeServer, room.roomId, contentUri);
// 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, user.homeServer, roomId2, contentUri);
});
test("should handle a sticker picker widget missing creatorUserId", async ({
webserver,
page,
app,
user,
room,
}) => {
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" });
const widgetHtml = getWidgetHtml(contentUri, "image/png");
stickerPickerUrl = webserver.start(widgetHtml);
await setWidgetAccountData(app, user, stickerPickerUrl, false);
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app);
await sendStickerFromPicker(page);
await expectTimelineSticker(page, user.homeServer, room.roomId, contentUri);
});
test("should render invalid mimetype as a file", async ({ webserver, page, app, user, room }) => {
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, {
type: "application/octet-stream",
});
const widgetHtml = getWidgetHtml(contentUri, "application/octet-stream");
stickerPickerUrl = webserver.start(widgetHtml);
await setWidgetAccountData(app, user, stickerPickerUrl);
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app);
await sendStickerFromPicker(page);
await expectFileTile(page, room.roomId, contentUri);
});
});
@@ -0,0 +1,161 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
Copyright 2022 Mikhail Aheichyk
Copyright 2022 Nordeck IT + Consulting GmbH.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import 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 { type 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();
});
}
});