Absorb the matrix-react-sdk repository (#28192)

Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Kim Brose <kim.brose@nordeck.net>
Co-authored-by: Florian Duros <florianduros@element.io>
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com>
Co-authored-by: ElementRobot <releases@riot.im>
Co-authored-by: dbkr <dbkr@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: David Langley <davidl@element.io>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
Co-authored-by: Timshel <Timshel@users.noreply.github.com>
Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com>
Co-authored-by: Will Hunt <will@half-shot.uk>
Co-authored-by: Hubert Chathi <hubert@uhoreg.ca>
Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
Co-authored-by: Robin <robin@robin.town>
Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
Michael Telatynski
2024-10-16 13:31:55 +01:00
committed by GitHub
parent 2b99496025
commit c05c429803
3280 changed files with 586617 additions and 905 deletions

View File

@@ -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
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",
);
});
});
});

View File

@@ -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
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", 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);
});
});

View File

@@ -0,0 +1,161 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
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";
const STICKER_NAME = "Test Sticker";
const ROOM_NAME_1 = "Sticker Test";
const ROOM_NAME_2 = "Sticker Test Two";
const STICKER_MESSAGE = JSON.stringify({
action: "m.sticker",
api: "fromWidget",
data: {
name: "teststicker",
description: STICKER_NAME,
file: "test.png",
content: {
body: STICKER_NAME,
msgtype: "m.sticker",
url: "mxc://localhost/somewhere",
},
},
requestId: "1",
widgetId: STICKER_PICKER_WIDGET_ID,
});
const WIDGET_HTML = `
<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(${STICKER_MESSAGE}, '*')
};
</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, roomId: string) {
// 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("/download/localhost/somewhere"),
);
}
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;
test.beforeEach(async ({ webserver }) => {
stickerPickerUrl = webserver.start(WIDGET_HTML);
});
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 });
await app.client.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: {
content: {
type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl,
creatorUserId: user.userId,
},
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
});
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);
});
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",
name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl,
// No creatorUserId
},
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
});
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${roomId1}`);
await openStickerPicker(app);
await sendStickerFromPicker(page);
await expectTimelineSticker(page, roomId1);
});
});

View File

@@ -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
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 { 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();
});
}
});