diff --git a/cypress/e2e/create-room/create-room.spec.ts b/cypress/e2e/create-room/create-room.spec.ts
index 1ebc1a7df7..a5a81b03f5 100644
--- a/cypress/e2e/create-room/create-room.spec.ts
+++ b/cypress/e2e/create-room/create-room.spec.ts
@@ -17,13 +17,6 @@ limitations under the License.
///
import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import Chainable = Cypress.Chainable;
-
-function openCreateRoomDialog(): Chainable> {
- cy.findByRole("button", { name: "Add room" }).click();
- cy.findByRole("menuitem", { name: "New room" }).click();
- return cy.get(".mx_CreateRoomDialog");
-}
describe("Create Room", () => {
let homeserver: HomeserverInstance;
@@ -44,7 +37,7 @@ describe("Create Room", () => {
const name = "Test room 1";
const topic = "This room is dedicated to this test and this test only!";
- openCreateRoomDialog().within(() => {
+ cy.openCreateRoomDialog().within(() => {
// Fill name & topic
cy.findByRole("textbox", { name: "Name" }).type(name);
cy.findByRole("textbox", { name: "Topic (optional)" }).type(topic);
diff --git a/cypress/e2e/knock/create-knock-room.spec.ts b/cypress/e2e/knock/create-knock-room.spec.ts
new file mode 100644
index 0000000000..dbbcf49492
--- /dev/null
+++ b/cypress/e2e/knock/create-knock-room.spec.ts
@@ -0,0 +1,136 @@
+/*
+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 { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { waitForRoom } from "../utils";
+import { Filter } from "../../support/settings";
+
+describe("Create Knock Room", () => {
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.enableLabsFeature("feature_ask_to_join");
+
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Alice");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should create a knock room", () => {
+ cy.openCreateRoomDialog().within(() => {
+ cy.findByRole("textbox", { name: "Name" }).type("Cybersecurity");
+ cy.findByRole("button", { name: "Room visibility" }).click();
+ cy.findByRole("option", { name: "Ask to join" }).click();
+
+ cy.findByRole("button", { name: "Create room" }).click();
+ });
+
+ cy.get(".mx_LegacyRoomHeader").within(() => {
+ cy.findByText("Cybersecurity");
+ });
+
+ cy.hash().then((urlHash) => {
+ const roomId = urlHash.replace("#/room/", "");
+
+ // Room should have a knock join rule
+ cy.window().then(async (win) => {
+ await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock",
+ );
+ });
+ });
+ });
+ });
+
+ it("should create a room and change a join rule to knock", () => {
+ cy.openCreateRoomDialog().within(() => {
+ cy.findByRole("textbox", { name: "Name" }).type("Cybersecurity");
+
+ cy.findByRole("button", { name: "Create room" }).click();
+ });
+
+ cy.get(".mx_LegacyRoomHeader").within(() => {
+ cy.findByText("Cybersecurity");
+ });
+
+ cy.hash().then((urlHash) => {
+ const roomId = urlHash.replace("#/room/", "");
+
+ cy.openRoomSettings("Security & Privacy");
+
+ cy.findByRole("group", { name: "Access" }).within(() => {
+ cy.findByRole("radio", { name: "Private (invite only)" }).should("be.checked");
+ cy.findByRole("radio", { name: "Ask to join" }).check({ force: true });
+ });
+
+ // Room should have a knock join rule
+ cy.window().then(async (win) => {
+ await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock",
+ );
+ });
+ });
+ });
+ });
+
+ it("should create a public knock room", () => {
+ cy.openCreateRoomDialog().within(() => {
+ cy.findByRole("textbox", { name: "Name" }).type("Cybersecurity");
+ cy.findByRole("button", { name: "Room visibility" }).click();
+ cy.findByRole("option", { name: "Ask to join" }).click();
+ cy.findByRole("checkbox", { name: "Make this room visible in the public room directory." }).click({
+ force: true,
+ });
+
+ cy.findByRole("button", { name: "Create room" }).click();
+ });
+
+ cy.get(".mx_LegacyRoomHeader").within(() => {
+ cy.findByText("Cybersecurity");
+ });
+
+ cy.hash().then((urlHash) => {
+ const roomId = urlHash.replace("#/room/", "");
+
+ // Room should have a knock join rule
+ cy.window().then(async (win) => {
+ await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock",
+ );
+ });
+ });
+ });
+
+ cy.openSpotlightDialog().within(() => {
+ cy.spotlightFilter(Filter.PublicRooms);
+ cy.spotlightResults().eq(0).should("contain", "Cybersecurity");
+ });
+ });
+});
diff --git a/cypress/e2e/knock/knock-into-room.spec.ts b/cypress/e2e/knock/knock-into-room.spec.ts
new file mode 100644
index 0000000000..c58d93db7e
--- /dev/null
+++ b/cypress/e2e/knock/knock-into-room.spec.ts
@@ -0,0 +1,200 @@
+/*
+Copyright 2023 Mikhail Aheichyk
+Copyright 2023 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 type { MatrixClient } from "matrix-js-sdk/src/matrix";
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { UserCredentials } from "../../support/login";
+import { waitForRoom } from "../utils";
+import { Filter } from "../../support/settings";
+
+describe("Knock Into Room", () => {
+ let homeserver: HomeserverInstance;
+ let user: UserCredentials;
+ let bot: MatrixClient;
+
+ let roomId;
+
+ beforeEach(() => {
+ cy.enableLabsFeature("feature_ask_to_join");
+
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Alice").then((_user) => {
+ user = _user;
+ });
+
+ cy.getBot(homeserver, { displayName: "Bob" }).then(async (_bot) => {
+ bot = _bot;
+
+ const { room_id: newRoomId } = await bot.createRoom({
+ name: "Cybersecurity",
+ initial_state: [
+ {
+ type: "m.room.join_rules",
+ content: {
+ join_rule: "knock",
+ },
+ state_key: "",
+ },
+ ],
+ });
+
+ roomId = newRoomId;
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should knock into the room then knock is approved and user joins the room", () => {
+ cy.viewRoomById(roomId);
+
+ cy.get(".mx_RoomPreviewBar").within(() => {
+ cy.findByRole("button", { name: "Join the discussion" }).click();
+
+ cy.findByRole("heading", { name: "Ask to join?" });
+ cy.findByRole("textbox");
+ cy.findByRole("button", { name: "Request access" }).click();
+
+ cy.findByRole("heading", { name: "Request to join sent" });
+ });
+
+ // Knocked room should appear in Rooms
+ cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" });
+
+ cy.window().then(async (win) => {
+ // bot waits for knock request from Alice
+ await waitForRoom(win, bot, roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) =>
+ e.getType() === "m.room.member" &&
+ e.getContent()?.membership === "knock" &&
+ e.getContent()?.displayname === "Alice",
+ );
+ });
+
+ // bot invites Alice
+ await bot.invite(roomId, user.userId);
+ });
+
+ cy.findByRole("group", { name: "Invites" }).findByRole("treeitem", { name: "Cybersecurity" });
+
+ // Alice have to accept invitation in order to join the room.
+ // It will be not needed when homeserver implements auto accept knock requests.
+ cy.get(".mx_RoomView").findByRole("button", { name: "Accept" }).click();
+
+ cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" });
+
+ cy.findByText("Alice joined the room").should("exist");
+ });
+
+ it("should knock into the room and knock is cancelled by user himself", () => {
+ cy.viewRoomById(roomId);
+
+ cy.get(".mx_RoomPreviewBar").within(() => {
+ cy.findByRole("button", { name: "Join the discussion" }).click();
+
+ cy.findByRole("heading", { name: "Ask to join?" });
+ cy.findByRole("textbox");
+ cy.findByRole("button", { name: "Request access" }).click();
+
+ cy.findByRole("heading", { name: "Request to join sent" });
+ });
+
+ // Knocked room should appear in Rooms
+ cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" });
+
+ cy.get(".mx_RoomPreviewBar").within(() => {
+ cy.findByRole("button", { name: "Cancel request" }).click();
+
+ cy.findByRole("heading", { name: "Ask to join Cybersecurity?" });
+ cy.findByRole("button", { name: "Request access" });
+ });
+
+ cy.findByRole("group", { name: "Historical" }).findByRole("treeitem", { name: "Cybersecurity" });
+ });
+
+ it("should knock into the room then knock is cancelled by another user and room is forgotten", () => {
+ cy.viewRoomById(roomId);
+
+ cy.get(".mx_RoomPreviewBar").within(() => {
+ cy.findByRole("button", { name: "Join the discussion" }).click();
+
+ cy.findByRole("heading", { name: "Ask to join?" });
+ cy.findByRole("textbox");
+ cy.findByRole("button", { name: "Request access" }).click();
+
+ cy.findByRole("heading", { name: "Request to join sent" });
+ });
+
+ // Knocked room should appear in Rooms
+ cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" });
+
+ cy.window().then(async (win) => {
+ // bot waits for knock request from Alice
+ await waitForRoom(win, bot, roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) =>
+ e.getType() === "m.room.member" &&
+ e.getContent()?.membership === "knock" &&
+ e.getContent()?.displayname === "Alice",
+ );
+ });
+
+ // bot kicks Alice
+ await bot.kick(roomId, user.userId);
+ });
+
+ // Room should stay in Rooms and have red badge when knock is denied
+ cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }).should("not.exist");
+ cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity 1 unread mention." });
+
+ cy.get(".mx_RoomPreviewBar").within(() => {
+ cy.findByRole("heading", { name: "You have been denied access" });
+ cy.findByRole("button", { name: "Forget this room" }).click();
+ });
+
+ // Room should disappear from the list completely when forgotten
+ // Should be enabled when issue is fixed: https://github.com/vector-im/element-web/issues/26195
+ // cy.findByRole("treeitem", { name: /Cybersecurity/ }).should("not.exist");
+ });
+
+ it("should knock into the public knock room via spotlight", () => {
+ cy.window().then((win) => {
+ bot.setRoomDirectoryVisibility(roomId, win.matrixcs.Visibility.Public);
+ });
+
+ cy.openSpotlightDialog().within(() => {
+ cy.spotlightFilter(Filter.PublicRooms);
+ cy.spotlightResults().eq(0).should("contain", "Cybersecurity");
+ cy.spotlightResults().eq(0).click();
+ });
+
+ cy.get(".mx_RoomPreviewBar").within(() => {
+ cy.findByRole("heading", { name: "Ask to join?" });
+ cy.findByRole("textbox");
+ cy.findByRole("button", { name: "Request access" }).click();
+
+ cy.findByRole("heading", { name: "Request to join sent" });
+ });
+ });
+});
diff --git a/cypress/e2e/knock/manage-knocks.spec.ts b/cypress/e2e/knock/manage-knocks.spec.ts
new file mode 100644
index 0000000000..f31f206a9b
--- /dev/null
+++ b/cypress/e2e/knock/manage-knocks.spec.ts
@@ -0,0 +1,142 @@
+/*
+Copyright 2023 Mikhail Aheichyk
+Copyright 2023 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 type { MatrixClient } from "matrix-js-sdk/src/matrix";
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { waitForRoom } from "../utils";
+
+describe("Manage Knocks", () => {
+ let homeserver: HomeserverInstance;
+ let bot: MatrixClient;
+ let roomId: string;
+
+ beforeEach(() => {
+ cy.enableLabsFeature("feature_ask_to_join");
+
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Alice");
+
+ cy.createRoom({
+ name: "Cybersecurity",
+ initial_state: [
+ {
+ type: "m.room.join_rules",
+ content: {
+ join_rule: "knock",
+ },
+ state_key: "",
+ },
+ ],
+ }).then((newRoomId) => {
+ roomId = newRoomId;
+ cy.viewRoomById(newRoomId);
+ });
+
+ cy.getBot(homeserver, { displayName: "Bob" }).then(async (_bot) => {
+ bot = _bot;
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should approve knock using bar", () => {
+ bot.knockRoom(roomId);
+
+ cy.get(".mx_RoomKnocksBar").within(() => {
+ cy.findByRole("heading", { name: "Asking to join" });
+ cy.findByText(/^Bob/);
+ cy.findByRole("button", { name: "Approve" }).click();
+ });
+
+ cy.get(".mx_RoomKnocksBar").should("not.exist");
+
+ cy.findByText("Alice invited Bob");
+ });
+
+ it("should deny knock using bar", () => {
+ bot.knockRoom(roomId);
+
+ cy.get(".mx_RoomKnocksBar").within(() => {
+ cy.findByRole("heading", { name: "Asking to join" });
+ cy.findByText(/^Bob/);
+ cy.findByRole("button", { name: "Deny" }).click();
+ });
+
+ cy.get(".mx_RoomKnocksBar").should("not.exist");
+
+ // Should receive Bob's "m.room.member" with "leave" membership when access is denied
+ cy.window().then(async (win) => {
+ await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) =>
+ e.getType() === "m.room.member" &&
+ e.getContent()?.membership === "leave" &&
+ e.getContent()?.displayname === "Bob",
+ );
+ });
+ });
+ });
+
+ it("should approve knock using people tab", () => {
+ bot.knockRoom(roomId, { reason: "Hello, can I join?" });
+
+ cy.openRoomSettings("People");
+
+ cy.findByRole("group", { name: "Asking to join" }).within(() => {
+ cy.findByText(/^Bob/);
+ cy.findByText("Hello, can I join?");
+ cy.findByRole("button", { name: "Approve" }).click();
+
+ cy.findByText(/^Bob/).should("not.exist");
+ });
+
+ cy.findByText("Alice invited Bob");
+ });
+
+ it("should deny knock using people tab", () => {
+ bot.knockRoom(roomId, { reason: "Hello, can I join?" });
+
+ cy.openRoomSettings("People");
+
+ cy.findByRole("group", { name: "Asking to join" }).within(() => {
+ cy.findByText(/^Bob/);
+ cy.findByText("Hello, can I join?");
+ cy.findByRole("button", { name: "Deny" }).click();
+
+ cy.findByText(/^Bob/).should("not.exist");
+ });
+
+ // Should receive Bob's "m.room.member" with "leave" membership when access is denied
+ cy.window().then(async (win) => {
+ await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => {
+ const events = room.getLiveTimeline().getEvents();
+ return events.some(
+ (e) =>
+ e.getType() === "m.room.member" &&
+ e.getContent()?.membership === "leave" &&
+ e.getContent()?.displayname === "Bob",
+ );
+ });
+ });
+ });
+});
diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts
index ee5532282f..f1c9842e8c 100644
--- a/cypress/e2e/spotlight/spotlight.spec.ts
+++ b/cypress/e2e/spotlight/spotlight.spec.ts
@@ -23,35 +23,12 @@ import Loggable = Cypress.Loggable;
import Timeoutable = Cypress.Timeoutable;
import Withinable = Cypress.Withinable;
import Shadow = Cypress.Shadow;
-
-enum Filter {
- People = "people",
- PublicRooms = "public_rooms",
-}
+import { Filter } from "../../support/settings";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
- /**
- * Opens the spotlight dialog
- */
- openSpotlightDialog(
- options?: Partial,
- ): Chainable>;
- spotlightDialog(
- options?: Partial,
- ): Chainable>;
- spotlightFilter(
- filter: Filter | null,
- options?: Partial,
- ): Chainable>;
- spotlightSearch(
- options?: Partial,
- ): Chainable>;
- spotlightResults(
- options?: Partial,
- ): Chainable>;
roomHeaderName(
options?: Partial,
): Chainable>;
@@ -60,57 +37,6 @@ declare global {
}
}
-Cypress.Commands.add(
- "openSpotlightDialog",
- (options?: Partial): Chainable> => {
- cy.get(".mx_RoomSearch_spotlightTrigger", options).click({ force: true });
- return cy.spotlightDialog(options);
- },
-);
-
-Cypress.Commands.add(
- "spotlightDialog",
- (options?: Partial): Chainable> => {
- return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
- },
-);
-
-Cypress.Commands.add(
- "spotlightFilter",
- (
- filter: Filter | null,
- options?: Partial,
- ): Chainable> => {
- let selector: string;
- switch (filter) {
- case Filter.People:
- selector = "#mx_SpotlightDialog_button_startChat";
- break;
- case Filter.PublicRooms:
- selector = "#mx_SpotlightDialog_button_explorePublicRooms";
- break;
- default:
- selector = ".mx_SpotlightDialog_filter";
- break;
- }
- return cy.get(selector, options).click();
- },
-);
-
-Cypress.Commands.add(
- "spotlightSearch",
- (options?: Partial): Chainable> => {
- return cy.get(".mx_SpotlightDialog_searchBox", options).findByRole("textbox", { name: "Search" });
- },
-);
-
-Cypress.Commands.add(
- "spotlightResults",
- (options?: Partial): Chainable> => {
- return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
- },
-);
-
Cypress.Commands.add(
"roomHeaderName",
(options?: Partial): Chainable> => {
diff --git a/cypress/e2e/utils.ts b/cypress/e2e/utils.ts
new file mode 100644
index 0000000000..0ffb125dae
--- /dev/null
+++ b/cypress/e2e/utils.ts
@@ -0,0 +1,52 @@
+/*
+Copyright 2023 Mikhail Aheichyk
+Copyright 2023 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 type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
+
+/**
+ * Resolves when room state matches predicate.
+ * @param win window object
+ * @param matrixClient MatrixClient instance that can be user or bot
+ * @param roomId room id to find room and check
+ * @param predicate defines condition that is used to check the room state
+ */
+export function waitForRoom(
+ win: Cypress.AUTWindow,
+ matrixClient: MatrixClient,
+ roomId: string,
+ predicate: (room: Room) => boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const room = matrixClient.getRoom(roomId);
+
+ if (predicate(room)) {
+ resolve();
+ return;
+ }
+
+ function onEvent(ev: MatrixEvent) {
+ if (ev.getRoomId() !== roomId) return;
+
+ if (predicate(room)) {
+ matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent);
+ resolve();
+ }
+ }
+
+ matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent);
+ });
+}
diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts
index a3c98b4f6f..58e4c09679 100644
--- a/cypress/e2e/widgets/events.spec.ts
+++ b/cypress/e2e/widgets/events.spec.ts
@@ -19,9 +19,10 @@ limitations under the License.
import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
-import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
+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";
@@ -68,30 +69,6 @@ const DEMO_WIDGET_HTML = `