Migrate widgets/* from Cypress to Playwright (#12032)

* Migrate send_event.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate read_events.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate kick.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate get-openid-token.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate layout.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate events.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate stickers.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate widget-pip-close.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add screenshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* expect.poll to stabilise test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2023-12-12 17:26:08 +00:00 committed by GitHub
parent 99ca613818
commit c9008152c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1439 additions and 1563 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
/// <reference types="cypress" /> import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { ElementAppPage } from "../../pages/ElementAppPage";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker"; const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers"; const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
@ -33,7 +33,7 @@ const STICKER_MESSAGE = JSON.stringify({
content: { content: {
body: STICKER_NAME, body: STICKER_NAME,
msgtype: "m.sticker", msgtype: "m.sticker",
url: "mxc://somewhere", url: "mxc://localhost/somewhere",
}, },
}, },
requestId: "1", requestId: "1",
@ -66,108 +66,86 @@ const WIDGET_HTML = `
</html> </html>
`; `;
function openStickerPicker() { async function openStickerPicker(app: ElementAppPage) {
cy.openMessageComposerOptions().findByRole("menuitem", { name: "Sticker" }).click(); const options = await app.openMessageComposerOptions();
await options.getByRole("menuitem", { name: "Sticker" }).click();
} }
function sendStickerFromPicker() { async function sendStickerFromPicker(page: Page) {
// Note: Until https://github.com/cypress-io/cypress/issues/136 is fixed we will need const iframe = page.frameLocator(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`);
// to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can await iframe.locator("#sendsticker").click();
// break into the iframe for us :(
cy.accessIframe(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`).within({}, () => {
cy.get("#sendsticker").should("exist").click();
});
// Sticker picker should close itself after sending. // 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 // 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 // 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 // using the thumbnail URL, but as soon as that fails, we will switch to the
// download URL. // 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 // 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 // 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 make sure that cross-origin postMessage works properly. This makes it difficult
// to write the test though, as we have to juggle iframe logistics. // to write the test though, as we have to juggle iframe logistics.
// //
// See sendStickerFromPicker() for more detail on iframe comms. // See sendStickerFromPicker() for more detail on iframe comms.
let stickerPickerUrl: string; let stickerPickerUrl: string;
let homeserver: HomeserverInstance; test.beforeEach(async ({ webserver }) => {
let userId: string; stickerPickerUrl = webserver.start(WIDGET_HTML);
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;
});
}); });
afterEach(() => { test("should send a sticker to multiple rooms", async ({ page, app, user }) => {
cy.stopHomeserver(homeserver); const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
cy.stopWebServers(); const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 });
});
it("should send a sticker to multiple rooms", () => { await app.client.setAccountData("m.widgets", {
cy.createRoom({
name: ROOM_NAME_1,
}).as("roomId1");
cy.createRoom({
name: ROOM_NAME_2,
}).as("roomId2");
cy.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: { [STICKER_PICKER_WIDGET_ID]: {
content: { content: {
type: "m.stickerpicker", type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME, name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl, url: stickerPickerUrl,
creatorUserId: userId, creatorUserId: user.userId,
}, },
sender: userId, sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID, state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget", type: "m.widget",
id: STICKER_PICKER_WIDGET_ID, 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", () => { test("should handle a sticker picker widget missing creatorUserId", async ({ page, app, user }) => {
cy.createRoom({ const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
name: ROOM_NAME_1,
}).as("roomId1"); await app.client.setAccountData("m.widgets", {
cy.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: { [STICKER_PICKER_WIDGET_ID]: {
content: { content: {
type: "m.stickerpicker", type: "m.stickerpicker",
@ -175,19 +153,17 @@ describe("Stickers", () => {
url: stickerPickerUrl, url: stickerPickerUrl,
// No creatorUserId // No creatorUserId
}, },
sender: userId, sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID, state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget", type: "m.widget",
id: STICKER_PICKER_WIDGET_ID, 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);
}); });
}); });

View file

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

View file

@ -25,6 +25,9 @@ declare global {
mxSettingsStore: { mxSettingsStore: {
setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void>; 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; matrixcs: typeof Matrix;
} }
} }

View file

@ -314,6 +314,54 @@ export class Client {
}, credentials); }, 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. * Sets the directory visibility for a room.
* @param roomId ID of the room to set the directory visibility for * @param roomId ID of the room to set the directory visibility for

View file

@ -33,7 +33,7 @@ export class Webserver {
const address = this.server.address() as AddressInfo; const address = this.server.address() as AddressInfo;
console.log(`Started webserver at ${address.address}:${address.port}`); console.log(`Started webserver at ${address.address}:${address.port}`);
return `http://localhost:${address.port}/`; return `http://localhost:${address.port}`;
} }
public stop(): void { public stop(): void {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB