diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts deleted file mode 100644 index f1c9842e8c..0000000000 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ /dev/null @@ -1,458 +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 { MatrixClient } from "../../global"; -import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import Chainable = Cypress.Chainable; -import Loggable = Cypress.Loggable; -import Timeoutable = Cypress.Timeoutable; -import Withinable = Cypress.Withinable; -import Shadow = Cypress.Shadow; -import { Filter } from "../../support/settings"; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Cypress { - interface Chainable { - roomHeaderName( - options?: Partial, - ): Chainable>; - startDM(name: string): Chainable; - } - } -} - -Cypress.Commands.add( - "roomHeaderName", - (options?: Partial): Chainable> => { - return cy.get(".mx_LegacyRoomHeader_nametext", options); - }, -); - -Cypress.Commands.add("startDM", (name: string) => { - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(name); - cy.wait(1000); // wait for the dialog code to settle - cy.get(".mx_Spinner").should("not.exist"); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", name); - cy.spotlightResults().eq(0).click(); - }); - // send first message to start DM - cy.findByRole("textbox", { name: "Send a message…" }).should("have.focus").type("Hey!{enter}"); - // The DM room is created at this point, this can take a little bit of time - cy.get(".mx_EventTile_body", { timeout: 30000 }).findByText("Hey!"); - cy.findByRole("group", { name: "People" }).findByText(name); -}); - -describe("Spotlight", () => { - let homeserver: HomeserverInstance; - - const bot1Name = "BotBob"; - let bot1: MatrixClient; - - const bot2Name = "ByteBot"; - let bot2: MatrixClient; - - const room1Name = "247"; - let room1Id: string; - - const room2Name = "Lounge"; - let room2Id: string; - - const room3Name = "Public"; - let room3Id: string; - - beforeEach(() => { - cy.startHomeserver("default").then((data) => { - homeserver = data; - cy.initTestUser(homeserver, "Jim") - .then(() => - cy.getBot(homeserver, { displayName: bot1Name }).then((_bot1) => { - bot1 = _bot1; - }), - ) - .then(() => - cy.getBot(homeserver, { displayName: bot2Name }).then((_bot2) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - bot2 = _bot2; - }), - ) - .then(() => - cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => { - cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(async (_room1Id) => { - room1Id = _room1Id; - await bot1.joinRoom(room1Id); - }); - bot2.createRoom({ name: room2Name, visibility: Visibility.Public }).then( - ({ room_id: _room2Id }) => { - room2Id = _room2Id; - bot2.invite(room2Id, bot1.getUserId()); - }, - ); - bot2.createRoom({ - name: room3Name, - visibility: Visibility.Public, - initial_state: [ - { - type: "m.room.history_visibility", - state_key: "", - content: { - history_visibility: "world_readable", - }, - }, - ], - }).then(({ room_id: _room3Id }) => { - room3Id = _room3Id; - bot2.invite(room3Id, bot1.getUserId()); - }); - }), - ) - .then(() => { - cy.visit("/#/room/" + room1Id); - cy.get(".mx_RoomSublist_skeletonUI").should("not.exist"); - }); - }); - // wait for the room to have the right name - cy.get(".mx_LegacyRoomHeader").within(() => { - cy.findByText(room1Name); - }); - }); - - afterEach(() => { - cy.visit("/#/home"); - cy.stopHomeserver(homeserver); - }); - - it("should be able to add and remove filters via keyboard", () => { - cy.openSpotlightDialog().within(() => { - cy.wait(1000); // wait for the dialog to settle, otherwise our keypresses might race with an update - - // initially, public spaces should be highlighted (because there are no other suggestions) - cy.get("#mx_SpotlightDialog_button_explorePublicSpaces").should("have.attr", "aria-selected", "true"); - - // hitting enter should enable the public rooms filter - cy.spotlightSearch().type("{enter}"); - cy.get(".mx_SpotlightDialog_filter").should("contain", "Public spaces"); - cy.spotlightSearch().type("{backspace}"); - cy.get(".mx_SpotlightDialog_filter").should("not.exist"); - cy.wait(200); // Again, wait to settle so keypresses arrive correctly - - cy.spotlightSearch().type("{downArrow}"); - cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); - cy.spotlightSearch().type("{enter}"); - cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms"); - cy.spotlightSearch().type("{backspace}"); - cy.get(".mx_SpotlightDialog_filter").should("not.exist"); - }); - }); - - it("should find joined rooms", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightSearch().clear().type(room1Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", room1Name); - cy.spotlightResults().eq(0).click(); - cy.url().should("contain", room1Id); - }) - .then(() => { - cy.roomHeaderName().should("contain", room1Name); - }); - }); - - it("should find known public rooms", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightSearch().clear().type(room1Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", room1Name); - cy.spotlightResults().eq(0).should("contain", "View"); - cy.spotlightResults().eq(0).click(); - cy.url().should("contain", room1Id); - }) - .then(() => { - cy.roomHeaderName().should("contain", room1Name); - }); - }); - - it("should find unknown public rooms", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightSearch().clear().type(room2Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", room2Name); - cy.spotlightResults().eq(0).should("contain", "Join"); - cy.spotlightResults().eq(0).click(); - cy.url().should("contain", room2Id); - }) - .then(() => { - cy.get(".mx_RoomView_MessageList").should("have.length", 1); - cy.roomHeaderName().should("contain", room2Name); - }); - }); - - it("should find unknown public world readable rooms", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightSearch().clear().type(room3Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", room3Name); - cy.spotlightResults().eq(0).should("contain", "View"); - cy.spotlightResults().eq(0).click(); - cy.url().should("contain", room3Id); - }) - .then(() => { - cy.findByRole("button", { name: "Join the discussion" }).click(); - cy.roomHeaderName().should("contain", room3Name); - }); - }); - - // TODO: We currently can’t test finding rooms on other homeservers/other protocols - // We obviously don’t have federation or bridges in cypress tests - it.skip("should find unknown public rooms on other homeservers", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightSearch().clear().type(room3Name); - cy.get("[aria-haspopup=true][role=button]").click(); - }) - .then(() => { - cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org") - .next("[role=menuitemradio]") - .click(); - cy.wait(3_600_000); - }) - .then(() => - cy.spotlightDialog().within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", room3Name); - cy.spotlightResults().eq(0).should("contain", room3Id); - }), - ); - }); - - it("should find known people", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot1Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", bot1Name); - cy.spotlightResults().eq(0).click(); - }) - .then(() => { - cy.roomHeaderName().should("contain", bot1Name); - }); - }); - - /** - * Search sends the correct query to Synapse. - * Synapse doesn't return the user in the result list. - * Waiting for the profile to be available via APIs before the tests didn't help. - * - * https://github.com/matrix-org/synapse/issues/16472 - */ - it.skip("should find unknown people", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot2Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", bot2Name); - cy.spotlightResults().eq(0).click(); - }) - .then(() => { - cy.roomHeaderName().should("contain", bot2Name); - }); - }); - - it("should find group DMs by usernames or user ids", () => { - // First we want to share a room with both bots to ensure we’ve got their usernames cached - cy.inviteUser(room1Id, bot2.getUserId()); - - // Starting a DM with ByteBot (will be turned into a group dm later) - cy.openSpotlightDialog().within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot2Name); - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", bot2Name); - cy.spotlightResults().eq(0).click(); - }); - - // Send first message to actually start DM - cy.roomHeaderName().should("contain", bot2Name); - cy.findByRole("textbox", { name: "Send a message…" }).type("Hey!{enter}"); - - // Assert DM exists by checking for the first message and the room being in the room list - cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 }); - cy.findByRole("group", { name: "People" }).should("contain", bot2Name); - - // Invite BotBob into existing DM with ByteBot - cy.getDmRooms(bot2.getUserId()) - .should("have.length", 1) - .then((dmRooms) => cy.getClient().then((client) => client.getRoom(dmRooms[0]))) - .then((groupDm) => { - cy.inviteUser(groupDm.roomId, bot1.getUserId()); - cy.roomHeaderName().should(($element) => expect($element.get(0).innerText).contains(groupDm.name)); - cy.findByRole("group", { name: "People" }).should(($element) => - expect($element.get(0).innerText).contains(groupDm.name), - ); - - // Search for BotBob by id, should return group DM and user - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot1.getUserId()); - cy.wait(1000); // wait for the dialog code to settle - cy.spotlightResults().should("have.length", 2); - cy.contains( - ".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", - groupDm.name, - ); - }); - - // Search for ByteBot by id, should return group DM and user - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot2.getUserId()); - cy.wait(1000); // wait for the dialog code to settle - cy.spotlightResults().should("have.length", 2); - cy.contains( - ".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", - groupDm.name, - ); - }); - }); - }); - - // Test against https://github.com/vector-im/element-web/issues/22851 - it("should show each person result only once", () => { - cy.openSpotlightDialog().within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - - // 2 rounds of search to simulate the bug conditions. Specifically, the first search - // should have 1 result (not 2) and the second search should also have 1 result (instead - // of the super buggy 3 described by https://github.com/vector-im/element-web/issues/22851) - // - // We search for user ID to trigger the profile lookup within the dialog. - for (let i = 0; i < 2; i++) { - cy.log("Iteration: " + i); - cy.spotlightSearch().clear().type(bot1.getUserId()); - cy.wait(1000); // wait for the dialog code to settle - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", bot1.getUserId()); - } - }); - }); - - it("should allow opening group chat dialog", () => { - cy.openSpotlightDialog() - .within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot2Name); - cy.wait(3000); // wait for the dialog code to settle - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", bot2Name); - cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat"); - cy.get(".mx_SpotlightDialog_startGroupChat").click(); - }) - .then(() => { - cy.findByRole("dialog").should("contain", "Direct Messages"); - }); - }); - - it("should close spotlight after starting a DM", () => { - cy.startDM(bot1Name); - cy.get(".mx_SpotlightDialog").should("have.length", 0); - }); - - it("should show the same user only once", () => { - cy.startDM(bot1Name); - cy.visit("/#/home"); - - cy.openSpotlightDialog().within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot1Name); - cy.wait(3000); // wait for the dialog code to settle - cy.get(".mx_Spinner").should("not.exist"); - cy.spotlightResults().should("have.length", 1); - }); - }); - - it("should be able to navigate results via keyboard", () => { - cy.openSpotlightDialog().within(() => { - cy.wait(500); // Wait for dialog to settle - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type("b"); - // our debouncing logic only starts the search after a short timeout, - // so we wait a few milliseconds. - cy.wait(1000); - cy.get(".mx_Spinner") - .should("not.exist") - .then(() => { - cy.wait(500); // Wait to settle again - cy.spotlightResults() - .should("have.length", 2) - .then(() => { - cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true"); - cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false"); - }); - cy.spotlightSearch() - .type("{downArrow}") - .then(() => { - cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false"); - cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true"); - }); - cy.spotlightSearch() - .type("{downArrow}") - .then(() => { - cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false"); - cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false"); - }); - cy.spotlightSearch() - .type("{upArrow}") - .then(() => { - cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false"); - cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true"); - }); - cy.spotlightSearch() - .type("{upArrow}") - .then(() => { - cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true"); - cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false"); - }); - }); - }); - }); -}); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts new file mode 100644 index 0000000000..177eccdc10 --- /dev/null +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -0,0 +1,393 @@ +/* +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"; +import { Filter } from "../../pages/Spotlight"; +import { Bot } from "../../pages/bot"; +import type { Locator, Page } from "@playwright/test"; +import type { ElementAppPage } from "../../pages/ElementAppPage"; + +function roomHeaderName(page: Page): Locator { + return page.locator(".mx_LegacyRoomHeader_nametext"); +} + +async function startDM(app: ElementAppPage, page: Page, name: string): Promise { + const spotlight = await app.openSpotlight(); + await spotlight.filter(Filter.People); + await spotlight.search(name); + await page.waitForTimeout(1000); // wait for the dialog code to settle + await expect(spotlight.dialog.locator(".mx_Spinner")).not.toBeAttached(); + const result = spotlight.results; + await expect(result).toHaveCount(1); + await expect(result.first()).toContainText(name); + await result.first().click(); + + // send first message to start DM + const locator = page.getByRole("textbox", { name: "Send a message…" }); + await expect(locator).toBeFocused(); + await locator.fill("Hey!"); + await locator.press("Enter"); + // The DM room is created at this point, this can take a little bit of time + await expect(page.locator(".mx_EventTile_body").getByText("Hey!")).toBeAttached({ timeout: 3000 }); + await expect(page.getByRole("group", { name: "People" }).getByText(name)).toBeAttached(); +} + +test.describe("Spotlight", () => { + const bot1Name = "BotBob"; + let bot1: Bot; + + const bot2Name = "ByteBot"; + let bot2: Bot; + + const room1Name = "247"; + let room1Id: string; + + const room2Name = "Lounge"; + let room2Id: string; + + const room3Name = "Public"; + let room3Id: string; + + test.use({ + displayName: "Jim", + }); + + test.beforeEach(async ({ page, homeserver, app, user }) => { + bot1 = new Bot(page, homeserver, { displayName: bot1Name, autoAcceptInvites: true }); + bot2 = new Bot(page, homeserver, { displayName: bot2Name, autoAcceptInvites: true }); + const Visibility = await page.evaluate(() => (window as any).matrixcs.Visibility); + + room1Id = await app.client.createRoom({ name: room1Name, visibility: Visibility.Public }); + + await bot1.joinRoom(room1Id); + const bot1UserId = await bot1.evaluate((client) => client.getUserId()); + room2Id = await bot2.createRoom({ name: room2Name, visibility: Visibility.Public }); + await bot2.inviteUser(room2Id, bot1UserId); + + room3Id = await bot2.createRoom({ + name: room3Name, + visibility: Visibility.Public, + initial_state: [ + { + type: "m.room.history_visibility", + state_key: "", + content: { + history_visibility: "world_readable", + }, + }, + ], + }); + await bot2.inviteUser(room3Id, bot1UserId); + + await page.goto("/#/room/" + room1Id); + await expect(page.locator(".mx_RoomSublist_skeletonUI")).not.toBeAttached(); + }); + + test("should be able to add and remove filters via keyboard", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(1000); // wait for the dialog to settle, otherwise our keypresses might race with an update + + // initially, public spaces should be highlighted (because there are no other suggestions) + await expect(spotlight.dialog.locator("#mx_SpotlightDialog_button_explorePublicSpaces")).toHaveAttribute( + "aria-selected", + "true", + ); + + // hitting enter should enable the public rooms filter + await spotlight.searchBox.press("Enter"); + await expect(spotlight.dialog.locator(".mx_SpotlightDialog_filter")).toHaveText("Public spaces"); + await spotlight.searchBox.press("Backspace"); + await expect(spotlight.dialog.locator(".mx_SpotlightDialog_filter")).not.toBeAttached(); + await page.waitForTimeout(200); // Again, wait to settle so keypresses arrive correctly + + await spotlight.searchBox.press("ArrowDown"); + await expect(spotlight.dialog.locator("#mx_SpotlightDialog_button_explorePublicRooms")).toHaveAttribute( + "aria-selected", + "true", + ); + await spotlight.searchBox.press("Enter"); + await expect(spotlight.dialog.locator(".mx_SpotlightDialog_filter")).toHaveText("Public rooms"); + await spotlight.searchBox.press("Backspace"); + await expect(spotlight.dialog.locator(".mx_SpotlightDialog_filter")).not.toBeAttached(); + }); + + test("should find joined rooms", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.search(room1Name); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(room1Name); + await resultLocator.first().click(); + expect(page.url()).toContain(room1Id); + await expect(roomHeaderName(page)).toContainText(room1Name); + }); + + test("should find known public rooms", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.PublicRooms); + await spotlight.search(room1Name); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(room1Name); + await expect(resultLocator.first()).toContainText("View"); + await resultLocator.first().click(); + expect(page.url()).toContain(room1Id); + await expect(roomHeaderName(page)).toContainText(room1Name); + }); + + test("should find unknown public rooms", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.PublicRooms); + await spotlight.search(room2Name); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(room2Name); + await expect(resultLocator.first()).toContainText("Join"); + await resultLocator.first().click(); + expect(page.url()).toContain(room2Id); + await expect(page.locator(".mx_RoomView_MessageList")).toHaveCount(1); + await expect(roomHeaderName(page)).toContainText(room2Name); + }); + + test("should find unknown public world readable rooms", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.PublicRooms); + await spotlight.search(room3Name); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(room3Name); + await expect(resultLocator.first()).toContainText("View"); + await resultLocator.first().click(); + expect(page.url()).toContain(room3Id); + await page.getByRole("button", { name: "Join the discussion" }).click(); + await expect(roomHeaderName(page)).toHaveText(room3Name); + }); + + // TODO: We currently can’t test finding rooms on other homeservers/other protocols + // We obviously don’t have federation or bridges in local e2e tests + test.skip("should find unknown public rooms on other homeservers", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.PublicRooms); + await spotlight.search(room3Name); + await page.locator("[aria-haspopup=true][role=button]").click(); + + await page + .locator(".mx_GenericDropdownMenu_Option--header") + .filter({ hasText: "matrix.org" }) + .locator("..") + .locator("[role=menuitemradio]") + .click(); + await page.waitForTimeout(3_600_000); + + await page.waitForTimeout(500); // wait for the dialog to settle + + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(room3Name); + await expect(resultLocator.first()).toContainText(room3Id); + }); + + test("should find known people", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + await spotlight.search(bot1Name); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(bot1Name); + await resultLocator.first().click(); + await expect(roomHeaderName(page)).toHaveText(bot1Name); + }); + + /** + * Search sends the correct query to Synapse. + * Synapse doesn't return the user in the result list. + * Waiting for the profile to be available via APIs before the tests didn't help. + * + * https://github.com/matrix-org/synapse/issues/16472 + */ + test.skip("should find unknown people", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + await spotlight.search(bot2Name); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(bot2Name); + await resultLocator.first().click(); + await expect(roomHeaderName(page)).toHaveText(bot2Name); + }); + + test("should find group DMs by usernames or user ids", async ({ page, app }) => { + // First we want to share a room with both bots to ensure we’ve got their usernames cached + const bot2UserId = await bot2.evaluate((client) => client.getUserId()); + await app.client.inviteUser(room1Id, bot2UserId); + + // Starting a DM with ByteBot (will be turned into a group dm later) + let spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + await spotlight.search(bot2Name); + let resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(bot2Name); + await resultLocator.first().click(); + + // Send first message to actually start DM + await expect(roomHeaderName(page)).toHaveText(bot2Name); + const locator = page.getByRole("textbox", { name: "Send a message…" }); + await locator.fill("Hey!"); + await locator.press("Enter"); + + // Assert DM exists by checking for the first message and the room being in the room list + await expect(page.locator(".mx_EventTile_body").filter({ hasText: "Hey!" })).toBeAttached({ timeout: 3000 }); + await expect(page.getByRole("group", { name: "People" })).toContainText(bot2Name); + + // Invite BotBob into existing DM with ByteBot + const dmRooms = await app.client.evaluate((client, userId) => { + const map = client.getAccountData("m.direct")?.getContent>(); + return map[userId] ?? []; + }, bot2UserId); + expect(dmRooms).toHaveLength(1); + const groupDmName = await app.client.evaluate((client, id) => client.getRoom(id).name, dmRooms[0]); + const bot1UserId = await bot1.evaluate((client) => client.getUserId()); + await app.client.inviteUser(dmRooms[0], bot1UserId); + await expect(roomHeaderName(page).first()).toContainText(groupDmName); + await expect(page.getByRole("group", { name: "People" }).first()).toContainText(groupDmName); + + // Search for BotBob by id, should return group DM and user + spotlight = await app.openSpotlight(); + await spotlight.filter(Filter.People); + await spotlight.search(bot1UserId); + await page.waitForTimeout(1000); // wait for the dialog to settle + resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(2); + await expect( + spotlight.dialog + .locator(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option") + .filter({ hasText: groupDmName }), + ).toBeAttached(); + + // Search for ByteBot by id, should return group DM and user + spotlight = await app.openSpotlight(); + await spotlight.filter(Filter.People); + await spotlight.search(bot2UserId); + await page.waitForTimeout(1000); // wait for the dialog to settle + resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(2); + await expect( + spotlight.dialog + .locator(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option") + .filter({ hasText: groupDmName }) + .last(), + ).toBeAttached(); + }); + + // Test against https://github.com/vector-im/element-web/issues/22851 + test("should show each person result only once", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + const bot1UserId = await bot1.evaluate((client) => client.getUserId()); + + // 2 rounds of search to simulate the bug conditions. Specifically, the first search + // should have 1 result (not 2) and the second search should also have 1 result (instead + // of the super buggy 3 described by https://github.com/vector-im/element-web/issues/22851) + // + // We search for user ID to trigger the profile lookup within the dialog. + for (let i = 0; i < 2; i++) { + console.log("Iteration: " + i); + await spotlight.search(bot1UserId); + await page.waitForTimeout(1000); // wait for the dialog to settle + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(bot1UserId); + } + }); + + test("should allow opening group chat dialog", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + await spotlight.search(bot2Name); + await page.waitForTimeout(3000); // wait for the dialog to settle + + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + await expect(resultLocator.first()).toContainText(bot2Name); + + await expect(spotlight.dialog.locator(".mx_SpotlightDialog_startGroupChat")).toContainText( + "Start a group chat", + ); + await spotlight.dialog.locator(".mx_SpotlightDialog_startGroupChat").click(); + await expect(page.getByRole("dialog")).toContainText("Direct Messages"); + }); + + test("should close spotlight after starting a DM", async ({ page, app }) => { + await startDM(app, page, bot1Name); + await expect(page.locator(".mx_SpotlightDialog")).toHaveCount(0); + }); + + test("should show the same user only once", async ({ page, app }) => { + await startDM(app, page, bot1Name); + await page.goto("/#/home"); + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + await spotlight.search(bot1Name); + await page.waitForTimeout(3000); // wait for the dialog to settle + await expect(spotlight.dialog.locator(".mx_Spinner")).not.toBeAttached(); + const resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(1); + }); + + test("should be able to navigate results via keyboard", async ({ page, app }) => { + const spotlight = await app.openSpotlight(); + await page.waitForTimeout(500); // wait for the dialog to settle + await spotlight.filter(Filter.People); + await spotlight.search("b"); + + let resultLocator = spotlight.results; + await expect(resultLocator).toHaveCount(2); + await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); + + await spotlight.searchBox.press("ArrowDown"); + resultLocator = spotlight.results; + await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true"); + + await spotlight.searchBox.press("ArrowDown"); + resultLocator = spotlight.results; + await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); + + await spotlight.searchBox.press("ArrowUp"); + resultLocator = spotlight.results; + await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true"); + + await spotlight.searchBox.press("ArrowUp"); + resultLocator = spotlight.results; + await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); + }); +}); diff --git a/playwright/pages/Spotlight.ts b/playwright/pages/Spotlight.ts index e638ab0aad..dcd4b73f85 100644 --- a/playwright/pages/Spotlight.ts +++ b/playwright/pages/Spotlight.ts @@ -28,8 +28,13 @@ export class Spotlight { constructor(private page: Page) {} public async open() { - await this.page.keyboard.press(`${CommandOrControl}+KeyK`); this.root = this.page.locator('[role=dialog][aria-label="Search Dialog"]'); + const isSpotlightAlreadyOpen = !!(await this.root.count()); + if (isSpotlightAlreadyOpen) { + // Close dialog if it is already open + await this.page.keyboard.press(`${CommandOrControl}+KeyK`); + } + await this.page.keyboard.press(`${CommandOrControl}+KeyK`); } public async filter(filter: Filter) { @@ -49,10 +54,18 @@ export class Spotlight { } public async search(query: string) { - await this.root.locator(".mx_SpotlightDialog_searchBox").getByRole("textbox", { name: "Search" }).fill(query); + await this.searchBox.getByRole("textbox", { name: "Search" }).fill(query); + } + + public get searchBox() { + return this.root.locator(".mx_SpotlightDialog_searchBox"); } public get results() { return this.root.locator(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option"); } + + public get dialog() { + return this.root; + } }