diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts
deleted file mode 100644
index 6f59e74401..0000000000
--- a/cypress/e2e/spaces/spaces.spec.ts
+++ /dev/null
@@ -1,347 +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.
-*/
-
-///
-
-import type { MatrixClient, Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import Chainable = Cypress.Chainable;
-import { UserCredentials } from "../../support/login";
-
-function openSpaceCreateMenu(): Chainable {
- cy.findByRole("button", { name: "Create a space" }).click();
- return cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu");
-}
-
-function openSpaceContextMenu(spaceName: string): Chainable {
- cy.getSpacePanelButton(spaceName).rightclick();
- return cy.get(".mx_SpacePanel_contextMenu");
-}
-
-function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
- return {
- creation_content: {
- type: "m.space",
- },
- initial_state: [
- {
- type: "m.room.name",
- content: {
- name: spaceName,
- },
- },
- ...roomIds.map(spaceChildInitialState),
- ],
- };
-}
-
-function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
- return {
- type: "m.space.child",
- state_key: roomId,
- content: {
- via: [roomId.split(":")[1]],
- },
- };
-}
-
-describe("Spaces", () => {
- let homeserver: HomeserverInstance;
- let user: UserCredentials;
-
- beforeEach(() => {
- cy.startHomeserver("default").then((data) => {
- homeserver = data;
-
- cy.initTestUser(homeserver, "Sue").then((_user) => {
- user = _user;
- cy.mockClipboard();
- });
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- it("should allow user to create public space", () => {
- openSpaceCreateMenu();
- cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu");
- cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => {
- // Regex pattern due to strings of "mx_SpaceCreateMenuType_public"
- cy.findByRole("button", { name: /Public/ }).click();
-
- cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
- "cypress/fixtures/riot.png",
- { force: true },
- );
- cy.findByRole("textbox", { name: "Name" }).type("Let's have a Riot");
- cy.findByRole("textbox", { name: "Address" }).should("have.value", "lets-have-a-riot");
- cy.findByRole("textbox", { name: "Description" }).type("This is a space to reminisce Riot.im!");
- cy.findByRole("button", { name: "Create" }).click();
- });
-
- // Create the default General & Random rooms, as well as a custom "Jokes" room
- cy.findByPlaceholderText("General").should("exist");
- cy.findByPlaceholderText("Random").should("exist");
- cy.findByPlaceholderText("Support").type("Jokes");
- cy.findByRole("button", { name: "Continue" }).click();
-
- // Copy matrix.to link
- // Regex pattern due to strings of "mx_SpacePublicShare_shareButton"
- cy.findByRole("button", { name: /Share invite link/ }).realClick();
- cy.getClipboardText().should("eq", "https://matrix.to/#/#lets-have-a-riot:localhost");
-
- // Go to space home
- cy.findByRole("button", { name: "Go to my first room" }).click();
-
- // Assert rooms exist in the room list
- cy.findByRole("treeitem", { name: "General" }).should("exist");
- cy.findByRole("treeitem", { name: "Random" }).should("exist");
- cy.findByRole("treeitem", { name: "Jokes" }).should("exist");
- });
-
- it("should allow user to create private space", () => {
- openSpaceCreateMenu().within(() => {
- // Regex pattern due to strings of "mx_SpaceCreateMenuType_private"
- cy.findByRole("button", { name: /Private/ }).click();
-
- cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
- "cypress/fixtures/riot.png",
- { force: true },
- );
- cy.findByRole("textbox", { name: "Name" }).type("This is not a Riot");
- cy.findByRole("textbox", { name: "Address" }).should("not.exist");
- cy.findByRole("textbox", { name: "Description" }).type("This is a private space of mourning Riot.im...");
- cy.findByRole("button", { name: "Create" }).click();
- });
-
- // Regex pattern due to strings of "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton"
- cy.findByRole("button", { name: /Me and my teammates/ }).click();
-
- // Create the default General & Random rooms, as well as a custom "Projects" room
- cy.findByPlaceholderText("General").should("exist");
- cy.findByPlaceholderText("Random").should("exist");
- cy.findByPlaceholderText("Support").type("Projects");
- cy.findByRole("button", { name: "Continue" }).click();
-
- cy.get(".mx_SpaceRoomView h1").findByText("Invite your teammates");
- cy.get(".mx_SpaceRoomView").percySnapshotElement("Space - 'Invite your teammates' dialog");
- cy.findByRole("button", { name: "Skip for now" }).click();
-
- // Assert rooms exist in the room list
- cy.findByRole("treeitem", { name: "General" }).should("exist");
- cy.findByRole("treeitem", { name: "Random" }).should("exist");
- cy.findByRole("treeitem", { name: "Projects" }).should("exist");
-
- // Assert rooms exist in the space explorer
- cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "General").should("exist");
- cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Random").should("exist");
- cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Projects").should("exist");
- });
-
- it("should allow user to create just-me space", () => {
- cy.createRoom({
- name: "Sample Room",
- });
-
- openSpaceCreateMenu().within(() => {
- // Regex pattern due to strings of "mx_SpaceCreateMenuType_private"
- cy.findByRole("button", { name: /Private/ }).click();
-
- cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
- "cypress/fixtures/riot.png",
- { force: true },
- );
- cy.findByRole("textbox", { name: "Address" }).should("not.exist");
- cy.findByRole("textbox", { name: "Description" }).type("This is a personal space to mourn Riot.im...");
- cy.findByRole("textbox", { name: "Name" }).type("This is my Riot{enter}");
- });
-
- // Regex pattern due to of strings of "mx_SpaceRoomView_privateScope_justMeButton"
- cy.findByRole("button", { name: /Just me/ }).click();
-
- cy.findByText("Sample Room").click({ force: true }); // force click as checkbox size is zero
-
- // Temporal implementation as multiple elements with the role "button" and name "Add" are found
- cy.get(".mx_AddExistingToSpace_footer").within(() => {
- cy.findByRole("button", { name: "Add" }).click();
- });
-
- cy.get(".mx_SpaceHierarchy_list").within(() => {
- // Regex pattern due to the strings of "mx_SpaceHierarchy_roomTile_joined"
- cy.findByRole("treeitem", { name: /Sample Room/ }).should("exist");
- });
- });
-
- it("should allow user to invite another to a space", () => {
- let bot: MatrixClient;
- cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
- bot = _bot;
- });
-
- cy.createSpace({
- visibility: "public" as any,
- room_alias_name: "space",
- }).as("spaceId");
-
- openSpaceContextMenu("#space:localhost").within(() => {
- cy.findByRole("menuitem", { name: "Invite" }).click();
- });
-
- cy.get(".mx_SpacePublicShare").within(() => {
- // Copy link first
- // Regex pattern due to strings of "mx_SpacePublicShare_shareButton"
- cy.findByRole("button", { name: /Share invite link/ })
- .focus()
- .realClick();
- cy.getClipboardText().should("eq", "https://matrix.to/#/#space:localhost");
- // Start Matrix invite flow
- // Regex pattern due to strings of "mx_SpacePublicShare_inviteButton"
- cy.findByRole("button", { name: /Invite people/ }).click();
- });
-
- cy.get(".mx_InviteDialog_other").within(() => {
- cy.findByRole("textbox").type(bot.getUserId());
- cy.findByRole("button", { name: "Invite" }).click();
- });
-
- cy.get(".mx_InviteDialog_other").should("not.exist");
- });
-
- it("should show space invites at the top of the space panel", () => {
- cy.createSpace({
- name: "My Space",
- });
- cy.getSpacePanelButton("My Space").should("exist");
-
- cy.getBot(homeserver, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
- const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space"));
- await bot.invite(roomId, user.userId);
- });
- // Assert that `Space Space` is above `My Space` due to it being an invite
- cy.getSpacePanelButton("Space Space")
- .should("exist")
- .parent()
- .next()
- .findByRole("button", { name: "My Space" })
- .should("exist");
- });
-
- it("should include rooms in space home", () => {
- cy.createRoom({
- name: "Music",
- }).as("roomId1");
- cy.createRoom({
- name: "Gaming",
- }).as("roomId2");
-
- const spaceName = "Spacey Mc. Space Space";
- cy.all([cy.get("@roomId1"), cy.get("@roomId2")]).then(([roomId1, roomId2]) => {
- cy.createSpace({
- name: spaceName,
- initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
- }).as("spaceId");
- });
-
- cy.get("@spaceId").then(() => {
- cy.viewSpaceHomeByName(spaceName);
- });
- cy.get(".mx_SpaceRoomView .mx_SpaceHierarchy_list").within(() => {
- // Regex pattern due to strings in "mx_SpaceHierarchy_roomTile_name"
- cy.findByRole("treeitem", { name: /Music/ }).findByRole("button").should("exist");
- cy.findByRole("treeitem", { name: /Gaming/ })
- .findByRole("button")
- .should("exist");
- });
- });
-
- it("should render subspaces in the space panel only when expanded", () => {
- cy.injectAxe();
-
- cy.createSpace({
- name: "Child Space",
- initial_state: [],
- }).then((spaceId) => {
- cy.createSpace({
- name: "Root Space",
- initial_state: [spaceChildInitialState(spaceId)],
- }).as("spaceId");
- });
-
- // Find collapsed Space panel
- cy.findByRole("tree", { name: "Spaces" }).within(() => {
- cy.findByRole("button", { name: "Root Space" }).should("exist");
- cy.findByRole("button", { name: "Child Space" }).should("not.exist");
- });
-
- const axeOptions = {
- rules: {
- // Disable this check as it triggers on nested roving tab index elements which are in practice fine
- "nested-interactive": {
- enabled: false,
- },
- // Disable this check as it wrongly triggers on the room list container which has
- // roving tab index elements with automatic scrolling
- "scrollable-region-focusable": {
- enabled: false,
- },
- },
- };
- cy.checkA11y(undefined, axeOptions);
- cy.get(".mx_SpacePanel").percySnapshotElement("Space panel collapsed", { widths: [68] });
-
- cy.findByRole("tree", { name: "Spaces" }).within(() => {
- // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
- // button with the same name with different class name "mx_SpacePanel_toggleCollapse".
- cy.findByRole("button", { name: "Expand" }).realHover().click();
- });
- cy.get(".mx_SpacePanel:not(.collapsed)").should("exist"); // TODO: replace :not() selector
-
- cy.contains(".mx_SpaceItem", "Root Space")
- .should("exist")
- .contains(".mx_SpaceItem", "Child Space")
- .should("exist");
-
- cy.checkA11y(undefined, axeOptions);
- cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] });
- });
-
- it("should not soft crash when joining a room from space hierarchy which has a link in its topic", () => {
- cy.getBot(homeserver, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
- const { room_id: roomId } = await bot.createRoom({
- preset: "public_chat" as Preset,
- name: "Test Room",
- topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
- });
- const { room_id: spaceId } = await bot.createRoom(spaceCreateOptions("Test Space", [roomId]));
- await bot.invite(spaceId, user.userId);
- });
-
- cy.getSpacePanelButton("Test Space").should("exist");
- cy.wait(500); // without this we can end up clicking too quickly and it ends up having no effect
- cy.viewSpaceByName("Test Space");
- cy.findByRole("button", { name: "Accept" }).click();
-
- // Regex pattern due to strings in "mx_SpaceHierarchy_roomTile_item"
- cy.findByRole("button", { name: /Test Room/ }).realHover();
- cy.findByRole("button", { name: "Join" }).should("exist").realHover().click();
- cy.findByRole("button", { name: "View", timeout: 5000 }).should("exist").realHover().click();
-
- // Assert we get shown the new room intro, and thus not the soft crash screen
- cy.get(".mx_NewRoomIntro").should("exist");
- });
-});
diff --git a/cypress/support/views.ts b/cypress/support/views.ts
index c610af5f8b..05a4e1e6ac 100644
--- a/cypress/support/views.ts
+++ b/cypress/support/views.ts
@@ -39,27 +39,6 @@ declare global {
* @param id
*/
viewRoomById(id: string): void;
-
- /**
- * Returns the space panel space button based on a name. The space
- * must be visible in the space panel
- * @param name The space name to find
- */
- getSpacePanelButton(name: string): Chainable>;
-
- /**
- * Opens the given space home by name. The space must be visible in
- * the space list.
- * @param name The space name to find and click on/open.
- */
- viewSpaceHomeByName(name: string): Chainable>;
-
- /**
- * Opens the given space by name. The space must be visible in the
- * space list.
- * @param name The space name to find and click on/open.
- */
- viewSpaceByName(name: string): Chainable>;
}
}
}
@@ -85,17 +64,5 @@ Cypress.Commands.add("viewRoomById", (id: string): void => {
cy.visit(`/#/room/${id}`);
});
-Cypress.Commands.add("getSpacePanelButton", (name: string): Chainable> => {
- return cy.findByRole("button", { name: name }).should("have.class", "mx_SpaceButton");
-});
-
-Cypress.Commands.add("viewSpaceByName", (name: string): Chainable> => {
- return cy.getSpacePanelButton(name).click();
-});
-
-Cypress.Commands.add("viewSpaceHomeByName", (name: string): Chainable> => {
- return cy.getSpacePanelButton(name).dblclick();
-});
-
// Needed to make this file a module
export {};
diff --git a/playwright.config.ts b/playwright.config.ts
index 92046df82e..4913d63e0f 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -26,6 +26,7 @@ export default defineConfig({
ignoreHTTPSErrors: true,
video: "retain-on-failure",
baseURL,
+ permissions: ["clipboard-write", "clipboard-read"],
},
webServer: {
command: process.env.CI ? "npx serve -p 8080 -L ../webapp" : "yarn --cwd ../element-web start",
diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts
new file mode 100644
index 0000000000..396ca803a5
--- /dev/null
+++ b/playwright/e2e/spaces/spaces.spec.ts
@@ -0,0 +1,296 @@
+/*
+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.
+*/
+
+import type { Locator, Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
+import { ElementAppPage } from "../../pages/ElementAppPage";
+
+async function openSpaceCreateMenu(page: Page): Promise {
+ await page.getByRole("button", { name: "Create a space" }).click();
+ return page.locator(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu");
+}
+
+async function openSpaceContextMenu(page: Page, app: ElementAppPage, spaceName: string): Promise {
+ const button = await app.getSpacePanelButton(spaceName);
+ await button.click({ button: "right" });
+ return page.locator(".mx_SpacePanel_contextMenu");
+}
+
+function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
+ return {
+ creation_content: {
+ type: "m.space",
+ },
+ initial_state: [
+ {
+ type: "m.room.name",
+ content: {
+ name: spaceName,
+ },
+ },
+ ...roomIds.map(spaceChildInitialState),
+ ],
+ };
+}
+
+function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
+ return {
+ type: "m.space.child",
+ state_key: roomId,
+ content: {
+ via: [roomId.split(":")[1]],
+ },
+ };
+}
+
+test.describe("Spaces", () => {
+ test.use({
+ displayName: "Sue",
+ botCreateOpts: { displayName: "BotBob" },
+ });
+
+ test("should allow user to create public space", async ({ page, app, user }) => {
+ const contextMenu = await openSpaceCreateMenu(page);
+ await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
+
+ await contextMenu.getByRole("button", { name: /Public/ }).click();
+
+ await contextMenu
+ .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
+ .setInputFiles("cypress/fixtures/riot.png");
+ await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot");
+ await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot");
+ await contextMenu.getByRole("textbox", { name: "Description" }).fill("This is a space to reminisce Riot.im!");
+ await contextMenu.getByRole("button", { name: "Create" }).click();
+
+ // Create the default General & Random rooms, as well as a custom "Jokes" room
+ await expect(page.getByPlaceholder("General")).toBeVisible();
+ await expect(page.getByPlaceholder("Random")).toBeVisible();
+ await page.getByPlaceholder("Support").fill("Jokes");
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ // Copy matrix.to link
+ await page.getByRole("button", { name: "Share invite link" }).click();
+ expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost");
+
+ // Go to space home
+ await page.getByRole("button", { name: "Go to my first room" }).click();
+
+ // Assert rooms exist in the room list
+ await expect(page.getByRole("treeitem", { name: "General" })).toBeVisible();
+ await expect(page.getByRole("treeitem", { name: "Random" })).toBeVisible();
+ await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
+ });
+
+ test("should allow user to create private space", async ({ page, app, user }) => {
+ const menu = await openSpaceCreateMenu(page);
+ await menu.getByRole("button", { name: "Private" }).click();
+
+ await menu
+ .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
+ .setInputFiles("cypress/fixtures/riot.png");
+ await menu.getByRole("textbox", { name: "Name" }).fill("This is not a Riot");
+ await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
+ await menu.getByRole("textbox", { name: "Description" }).fill("This is a private space of mourning Riot.im...");
+ await menu.getByRole("button", { name: "Create" }).click();
+
+ await page.getByRole("button", { name: "Me and my teammates" }).click();
+
+ // Create the default General & Random rooms, as well as a custom "Projects" room
+ await expect(page.getByPlaceholder("General")).toBeVisible();
+ await expect(page.getByPlaceholder("Random")).toBeVisible();
+ await page.getByPlaceholder("Support").fill("Projects");
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ await expect(page.locator(".mx_SpaceRoomView h1").getByText("Invite your teammates")).toBeVisible();
+ await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("invite-teammates-dialog.png");
+ await page.getByRole("button", { name: "Skip for now" }).click();
+
+ // Assert rooms exist in the room list
+ await expect(page.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
+ await expect(page.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
+ await expect(page.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
+
+ // Assert rooms exist in the space explorer
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "General" }),
+ ).toBeVisible();
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Random" }),
+ ).toBeVisible();
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Projects" }),
+ ).toBeVisible();
+ });
+
+ test("should allow user to create just-me space", async ({ page, app, user }) => {
+ await app.client.createRoom({
+ name: "Sample Room",
+ });
+
+ const menu = await openSpaceCreateMenu(page);
+ await menu.getByRole("button", { name: "Private" }).click();
+
+ await menu
+ .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
+ .setInputFiles("cypress/fixtures/riot.png");
+ await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
+ await menu.getByRole("textbox", { name: "Description" }).fill("This is a personal space to mourn Riot.im...");
+ await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot");
+ await menu.getByRole("textbox", { name: "Name" }).press("Enter");
+
+ await page.getByRole("button", { name: "Just me" }).click();
+
+ await page.getByText("Sample Room").click({ force: true }); // force click as checkbox size is zero
+
+ // Temporal implementation as multiple elements with the role "button" and name "Add" are found
+ await page.locator(".mx_AddExistingToSpace_footer").getByRole("button", { name: "Add" }).click();
+
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list").getByRole("treeitem", { name: "Sample Room" }),
+ ).toBeVisible();
+ });
+
+ test("should allow user to invite another to a space", async ({ page, app, user, bot }) => {
+ await app.client.createSpace({
+ visibility: "public" as any,
+ room_alias_name: "space",
+ });
+
+ const menu = await openSpaceContextMenu(page, app, "#space:localhost");
+ await menu.getByRole("menuitem", { name: "Invite" }).click();
+
+ const shareDialog = page.locator(".mx_SpacePublicShare");
+ // Copy link first
+ await shareDialog.getByRole("button", { name: "Share invite link" }).click();
+ expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#space:localhost");
+ // Start Matrix invite flow
+ await shareDialog.getByRole("button", { name: "Invite people" }).click();
+
+ const otherSection = page.locator(".mx_InviteDialog_other");
+ await otherSection.getByRole("textbox").fill(bot.credentials.userId);
+ await otherSection.getByRole("button", { name: "Invite" }).click();
+
+ await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
+ });
+
+ test("should show space invites at the top of the space panel", async ({ page, app, user, bot }) => {
+ await app.client.createSpace({
+ name: "My Space",
+ });
+ await expect(await app.getSpacePanelButton("My Space")).toBeVisible();
+
+ const roomId = await bot.createRoom(spaceCreateOptions("Space Space"));
+ await bot.inviteUser(roomId, user.userId);
+
+ // Assert that `Space Space` is above `My Space` due to it being an invite
+ const buttons = page.getByRole("tree", { name: "Spaces" }).locator(".mx_SpaceButton");
+ await expect(buttons.nth(1)).toHaveAttribute("aria-label", "Space Space");
+ await expect(buttons.nth(2)).toHaveAttribute("aria-label", "My Space");
+ });
+
+ test("should include rooms in space home", async ({ page, app, user }) => {
+ const roomId1 = await app.client.createRoom({
+ name: "Music",
+ });
+ const roomId2 = await app.client.createRoom({
+ name: "Gaming",
+ });
+
+ const spaceName = "Spacey Mc. Space Space";
+ await app.client.createSpace({
+ name: spaceName,
+ initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
+ });
+
+ await app.viewSpaceHomeByName(spaceName);
+
+ const hierarchyList = page.locator(".mx_SpaceRoomView .mx_SpaceHierarchy_list");
+ await expect(hierarchyList.getByRole("treeitem", { name: "Music" }).getByRole("button")).toBeVisible();
+ await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible();
+ });
+
+ test("should render subspaces in the space panel only when expanded", async ({
+ page,
+ app,
+ user,
+ axe,
+ checkA11y,
+ }) => {
+ axe.disableRules([
+ // Disable this check as it triggers on nested roving tab index elements which are in practice fine
+ "nested-interactive",
+ // XXX: We have some known contrast issues here
+ "color-contrast",
+ ]);
+
+ const childSpaceId = await app.client.createSpace({
+ name: "Child Space",
+ initial_state: [],
+ });
+ await app.client.createSpace({
+ name: "Root Space",
+ initial_state: [spaceChildInitialState(childSpaceId)],
+ });
+
+ // Find collapsed Space panel
+ const spaceTree = page.getByRole("tree", { name: "Spaces" });
+ await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
+ await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
+
+ await checkA11y();
+ await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png");
+
+ // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
+ // button with the same name with different class name "mx_SpacePanel_toggleCollapse".
+ await spaceTree.getByRole("button", { name: "Expand" }).click();
+ await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector
+
+ const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" });
+ await expect(item).toBeVisible();
+ await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
+
+ await checkA11y();
+ await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
+ });
+
+ test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({
+ page,
+ app,
+ user,
+ bot,
+ }) => {
+ const roomId = await bot.createRoom({
+ preset: "public_chat" as Preset,
+ name: "Test Room",
+ topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
+ });
+ const spaceId = await bot.createRoom(spaceCreateOptions("Test Space", [roomId]));
+ await bot.inviteUser(spaceId, user.userId);
+
+ await expect(await app.getSpacePanelButton("Test Space")).toBeVisible();
+ await app.viewSpaceByName("Test Space");
+ await page.getByRole("button", { name: "Accept" }).click();
+
+ await page.getByRole("button", { name: "Test Room" }).hover();
+ await page.getByRole("button", { name: "Join", exact: true }).click();
+ await page.getByRole("button", { name: "View", exact: true }).click();
+
+ // Assert we get shown the new room intro, and thus not the soft crash screen
+ await expect(page.locator(".mx_NewRoomIntro")).toBeVisible();
+ });
+});
diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts
index a602aadb87..6cb6c27e90 100644
--- a/playwright/pages/ElementAppPage.ts
+++ b/playwright/pages/ElementAppPage.ts
@@ -117,4 +117,18 @@ export class ElementAppPage {
const button = await this.getSpacePanelButton(name);
return button.dblclick();
}
+
+ /**
+ * Opens the given space by name. The space must be visible in the
+ * space list.
+ * @param name The space name to find and click on/open.
+ */
+ public async viewSpaceByName(name: string): Promise {
+ const button = await this.getSpacePanelButton(name);
+ return button.click();
+ }
+
+ public async getClipboardText(): Promise {
+ return this.page.evaluate("navigator.clipboard.readText()");
+ }
}
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index 1b3c7dc092..7729442903 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -161,4 +161,17 @@ export class Client {
},
);
}
+
+ /**
+ * Invites the given user to the given room.
+ * @param roomId the id of the room to invite to
+ * @param userId the id of the user to invite
+ */
+ public async inviteUser(roomId: string, userId: string): Promise {
+ const client = await this.prepareClient();
+ await client.evaluate((client, { roomId, userId }) => client.invite(roomId, userId), {
+ roomId,
+ userId,
+ });
+ }
}
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png
new file mode 100644
index 0000000000..5139b5b9b1
Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png
new file mode 100644
index 0000000000..f8229c5dd9
Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png
new file mode 100644
index 0000000000..69c021429c
Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png
new file mode 100644
index 0000000000..0ad317d37d
Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ