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