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;
+ }
}