Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5096e7b

Browse files
authored
Integrate searching public rooms and people into the new search experience (#8707)
* Implement searching for public rooms and users in new search experience * Implement loading indicator for spotlight results * Moved spotlight dialog into own subfolder * Extract search result avatar into separate component * Build generic new dropdown menu component * Build new network menu based on new network dropdown component * Switch roomdirectory to use new network dropdown * Replace old networkdropdown with new networkdropdown * Added component for public room result details * Extract hooks and subcomponents from SpotlightDialog * Create new hook to get profile info based for an mxid * Add hook to automatically re-request search results * Add hook to prevent out-of-order search results * Extract member sort algorithm from InviteDialog * Keep sorting for non-room results stable * Sort people suggestions using sort algorithm from InviteDialog * Add copy/copied tooltip for invite link option in spotlight * Clamp length of topic for public room results * Add unit test for useDebouncedSearch * Add unit test for useProfileInfo * Create cypress test cases for spotlight dialog * Add test for useLatestResult to prevent out-of-order results
1 parent 37298d7 commit 5096e7b

38 files changed

+3520
-1397
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import { MatrixClient } from "../../global";
20+
import { SynapseInstance } from "../../plugins/synapsedocker";
21+
import Chainable = Cypress.Chainable;
22+
import Loggable = Cypress.Loggable;
23+
import Timeoutable = Cypress.Timeoutable;
24+
import Withinable = Cypress.Withinable;
25+
import Shadow = Cypress.Shadow;
26+
27+
export enum Filter {
28+
People = "people",
29+
PublicRooms = "public_rooms"
30+
}
31+
32+
declare global {
33+
// eslint-disable-next-line @typescript-eslint/no-namespace
34+
namespace Cypress {
35+
interface Chainable {
36+
/**
37+
* Opens the spotlight dialog
38+
*/
39+
openSpotlightDialog(
40+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
41+
): Chainable<JQuery<HTMLElement>>;
42+
spotlightDialog(
43+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
44+
): Chainable<JQuery<HTMLElement>>;
45+
spotlightFilter(
46+
filter: Filter | null,
47+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
48+
): Chainable<JQuery<HTMLElement>>;
49+
spotlightSearch(
50+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
51+
): Chainable<JQuery<HTMLElement>>;
52+
spotlightResults(
53+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
54+
): Chainable<JQuery<HTMLElement>>;
55+
roomHeaderName(
56+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
57+
): Chainable<JQuery<HTMLElement>>;
58+
}
59+
}
60+
}
61+
62+
Cypress.Commands.add("openSpotlightDialog", (
63+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
64+
): Chainable<JQuery<HTMLElement>> => {
65+
cy.get('.mx_RoomSearch_spotlightTrigger', options).click({ force: true });
66+
return cy.spotlightDialog(options);
67+
});
68+
69+
Cypress.Commands.add("spotlightDialog", (
70+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
71+
): Chainable<JQuery<HTMLElement>> => {
72+
return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
73+
});
74+
75+
Cypress.Commands.add("spotlightFilter", (
76+
filter: Filter | null,
77+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
78+
): Chainable<JQuery<HTMLElement>> => {
79+
let selector: string;
80+
switch (filter) {
81+
case Filter.People:
82+
selector = "#mx_SpotlightDialog_button_startChat";
83+
break;
84+
case Filter.PublicRooms:
85+
selector = "#mx_SpotlightDialog_button_explorePublicRooms";
86+
break;
87+
default:
88+
selector = ".mx_SpotlightDialog_filter";
89+
break;
90+
}
91+
return cy.get(selector, options).click();
92+
});
93+
94+
Cypress.Commands.add("spotlightSearch", (
95+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
96+
): Chainable<JQuery<HTMLElement>> => {
97+
return cy.get(".mx_SpotlightDialog_searchBox input", options);
98+
});
99+
100+
Cypress.Commands.add("spotlightResults", (
101+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
102+
): Chainable<JQuery<HTMLElement>> => {
103+
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
104+
});
105+
106+
Cypress.Commands.add("roomHeaderName", (
107+
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
108+
): Chainable<JQuery<HTMLElement>> => {
109+
return cy.get(".mx_RoomHeader_nametext", options);
110+
});
111+
112+
describe("Spotlight", () => {
113+
let synapse: SynapseInstance;
114+
115+
const bot1Name = "BotBob";
116+
let bot1: MatrixClient;
117+
118+
const bot2Name = "ByteBot";
119+
let bot2: MatrixClient;
120+
121+
const room1Name = "247";
122+
let room1Id: string;
123+
124+
const room2Name = "Lounge";
125+
let room2Id: string;
126+
127+
beforeEach(() => {
128+
cy.enableLabsFeature("feature_spotlight");
129+
cy.startSynapse("default").then(data => {
130+
synapse = data;
131+
cy.initTestUser(synapse, "Jim").then(() =>
132+
cy.getBot(synapse, bot1Name).then(_bot1 => {
133+
bot1 = _bot1;
134+
}),
135+
).then(() =>
136+
cy.getBot(synapse, bot2Name).then(_bot2 => {
137+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
138+
bot2 = _bot2;
139+
}),
140+
).then(() =>
141+
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
142+
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => {
143+
room1Id = _room1Id;
144+
cy.inviteUser(room1Id, bot1.getUserId());
145+
cy.visit("/#/room/" + room1Id);
146+
});
147+
bot2.createRoom({ name: room2Name, visibility: Visibility.Public })
148+
.then(({ room_id: _room2Id }) => {
149+
room2Id = _room2Id;
150+
bot2.invite(room2Id, bot1.getUserId());
151+
});
152+
}),
153+
).then(() =>
154+
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
155+
);
156+
});
157+
});
158+
159+
afterEach(() => {
160+
cy.stopSynapse(synapse);
161+
});
162+
163+
it("should be able to add and remove filters via keyboard", () => {
164+
cy.openSpotlightDialog().within(() => {
165+
cy.spotlightSearch().type("{downArrow}");
166+
cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true");
167+
cy.spotlightSearch().type("{enter}");
168+
cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms");
169+
cy.spotlightSearch().type("{backspace}");
170+
cy.get(".mx_SpotlightDialog_filter").should("not.exist");
171+
172+
cy.spotlightSearch().type("{downArrow}");
173+
cy.spotlightSearch().type("{downArrow}");
174+
cy.get("#mx_SpotlightDialog_button_startChat").should("have.attr", "aria-selected", "true");
175+
cy.spotlightSearch().type("{enter}");
176+
cy.get(".mx_SpotlightDialog_filter").should("contain", "People");
177+
cy.spotlightSearch().type("{backspace}");
178+
cy.get(".mx_SpotlightDialog_filter").should("not.exist");
179+
});
180+
});
181+
182+
it("should find joined rooms", () => {
183+
cy.openSpotlightDialog().within(() => {
184+
cy.spotlightSearch().clear().type(room1Name);
185+
cy.spotlightResults().should("have.length", 1);
186+
cy.spotlightResults().eq(0).should("contain", room1Name);
187+
cy.spotlightResults().eq(0).click();
188+
cy.url().should("contain", room1Id);
189+
}).then(() => {
190+
cy.roomHeaderName().should("contain", room1Name);
191+
});
192+
});
193+
194+
it("should find known public rooms", () => {
195+
cy.openSpotlightDialog().within(() => {
196+
cy.spotlightFilter(Filter.PublicRooms);
197+
cy.spotlightSearch().clear().type(room1Name);
198+
cy.spotlightResults().should("have.length", 1);
199+
cy.spotlightResults().eq(0).should("contain", room1Name);
200+
cy.spotlightResults().eq(0).click();
201+
cy.url().should("contain", room1Id);
202+
}).then(() => {
203+
cy.roomHeaderName().should("contain", room1Name);
204+
});
205+
});
206+
207+
it("should find unknown public rooms", () => {
208+
cy.openSpotlightDialog().within(() => {
209+
cy.spotlightFilter(Filter.PublicRooms);
210+
cy.spotlightSearch().clear().type(room2Name);
211+
cy.spotlightResults().should("have.length", 1);
212+
cy.spotlightResults().eq(0).should("contain", room2Name);
213+
cy.spotlightResults().eq(0).click();
214+
cy.url().should("contain", room2Id);
215+
}).then(() => {
216+
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
217+
cy.roomHeaderName().should("contain", room2Name);
218+
});
219+
});
220+
221+
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
222+
// We obviously don’t have federation or bridges in cypress tests
223+
/*
224+
const room3Name = "Matrix HQ";
225+
const room3Id = "#matrix:matrix.org";
226+
227+
it("should find unknown public rooms on other homeservers", () => {
228+
cy.openSpotlightDialog().within(() => {
229+
cy.spotlightFilter(Filter.PublicRooms);
230+
cy.spotlightSearch().clear().type(room3Name);
231+
cy.get("[aria-haspopup=true][role=button]").click();
232+
}).then(() => {
233+
cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org")
234+
.next("[role=menuitemradio]")
235+
.click();
236+
cy.wait(3_600_000);
237+
}).then(() => cy.spotlightDialog().within(() => {
238+
cy.spotlightResults().should("have.length", 1);
239+
cy.spotlightResults().eq(0).should("contain", room3Name);
240+
cy.spotlightResults().eq(0).should("contain", room3Id);
241+
}));
242+
});
243+
*/
244+
it("should find known people", () => {
245+
cy.openSpotlightDialog().within(() => {
246+
cy.spotlightFilter(Filter.People);
247+
cy.spotlightSearch().clear().type(bot1Name);
248+
cy.spotlightResults().should("have.length", 1);
249+
cy.spotlightResults().eq(0).should("contain", bot1Name);
250+
cy.spotlightResults().eq(0).click();
251+
}).then(() => {
252+
cy.roomHeaderName().should("contain", bot1Name);
253+
});
254+
});
255+
256+
it("should find unknown people", () => {
257+
cy.openSpotlightDialog().within(() => {
258+
cy.spotlightFilter(Filter.People);
259+
cy.spotlightSearch().clear().type(bot2Name);
260+
cy.spotlightResults().should("have.length", 1);
261+
cy.spotlightResults().eq(0).should("contain", bot2Name);
262+
cy.spotlightResults().eq(0).click();
263+
}).then(() => {
264+
cy.roomHeaderName().should("contain", bot2Name);
265+
});
266+
});
267+
268+
it("should allow opening group chat dialog", () => {
269+
cy.openSpotlightDialog().within(() => {
270+
cy.spotlightFilter(Filter.People);
271+
cy.spotlightSearch().clear().type(bot2Name);
272+
cy.spotlightResults().should("have.length", 1);
273+
cy.spotlightResults().eq(0).should("contain", bot2Name);
274+
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
275+
cy.get(".mx_SpotlightDialog_startGroupChat").click();
276+
}).then(() => {
277+
cy.get('[role=dialog]').should("contain", "Direct Messages");
278+
});
279+
});
280+
281+
it("should be able to navigate results via keyboard", () => {
282+
cy.openSpotlightDialog().within(() => {
283+
cy.spotlightFilter(Filter.People);
284+
cy.spotlightSearch().clear().type("b");
285+
cy.spotlightResults().should("have.length", 2);
286+
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true");
287+
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
288+
cy.spotlightSearch().type("{downArrow}");
289+
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
290+
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true");
291+
cy.spotlightSearch().type("{downArrow}");
292+
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
293+
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
294+
cy.spotlightSearch().type("{upArrow}");
295+
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
296+
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true");
297+
cy.spotlightSearch().type("{upArrow}");
298+
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true");
299+
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
300+
});
301+
});
302+
});

cypress/integration/5-threads/threads.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ describe("Threads", () => {
2828
let synapse: SynapseInstance;
2929

3030
beforeEach(() => {
31+
// Default threads to ON for this spec
32+
cy.enableLabsFeature("feature_thread");
3133
cy.window().then(win => {
3234
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
33-
win.localStorage.setItem("mx_labs_feature_feature_thread", "true"); // Default threads to ON for this spec
3435
});
3536
cy.startSynapse("default").then(data => {
3637
synapse = data;

cypress/support/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import "cypress-real-events";
2222
import "./performance";
2323
import "./synapse";
2424
import "./login";
25+
import "./labs";
2526
import "./client";
2627
import "./settings";
2728
import "./bot";

cypress/support/labs.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import Chainable = Cypress.Chainable;
18+
19+
/// <reference types="cypress" />
20+
21+
declare global {
22+
// eslint-disable-next-line @typescript-eslint/no-namespace
23+
namespace Cypress {
24+
interface Chainable {
25+
/**
26+
* Enables a labs feature for an element session.
27+
* Has to be called before the session is initialized
28+
* @param feature labsFeature to enable (e.g. "feature_spotlight")
29+
*/
30+
enableLabsFeature(feature: string): Chainable<null>;
31+
}
32+
}
33+
}
34+
35+
Cypress.Commands.add("enableLabsFeature", (feature: string): Chainable<null> => {
36+
return cy.window({ log: false }).then(win => {
37+
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
38+
}).then(() => null);
39+
});
40+
41+
// Needed to make this file a module
42+
export { };

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
@import "./structures/_FileDropTarget.scss";
3535
@import "./structures/_FilePanel.scss";
3636
@import "./structures/_GenericErrorPage.scss";
37+
@import "./structures/_GenericDropdownMenu.scss";
3738
@import "./structures/_HeaderButtons.scss";
3839
@import "./structures/_HomePage.scss";
3940
@import "./structures/_LeftPanel.scss";

0 commit comments

Comments
 (0)