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

Commit f3f14af

Browse files
Move spaces tests from Puppeteer to Cypress (#8645)
* Move spaces tests from Puppeteer to Cypress * Add missing fixture * Tweak synapsedocker to not double error on a docker failure * Fix space hierarchy loading race condition Fixes element-hq/element-web-rageshakes#10345 * Fix race condition when creating public space with url update code * Try Electron once more due to perms issues around clipboard * Try set browser permissions properly * Try to enable clipboard another way * Try electron again * Try electron again again * Switch to built-in cypress feature for file uploads * Mock clipboard instead * TMPDIR ftw? * uid:gid pls * Clipboard tests can now run on any browser due to mocking * Test Enter as well as button for space creation * Make the test actually work * Update cypress/support/util.ts Co-authored-by: Eric Eastwood <[email protected]> Co-authored-by: Eric Eastwood <[email protected]>
1 parent d75e2f1 commit f3f14af

File tree

21 files changed

+492
-148
lines changed

21 files changed

+492
-148
lines changed

.github/workflows/element-build-and-test.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ jobs:
7171
# to run the tests, so use chrome.
7272
browser: chrome
7373
start: npx serve -p 8080 webapp
74+
wait-on: 'http://localhost:8080'
7475
record: true
7576
command-prefix: 'yarn percy exec --'
7677
env:
@@ -83,6 +84,8 @@ jobs:
8384
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
8485
# pass GitHub token to allow accurately detecting a build vs a re-run build
8586
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87+
# make Node's os.tmpdir() return something where we actually have permissions
88+
TMPDIR: ${{ runner.temp }}
8689

8790
- name: Upload Artifact
8891
if: failure()

.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,4 @@ package-lock.json
2626
/cypress/synapselogs
2727
# These could have files in them but don't currently
2828
# Cypress will still auto-create them though...
29-
/cypress/fixtures
3029
/cypress/performance

cypress/fixtures/riot.png

13.5 KB
Loading

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ describe("Threads", () => {
8787
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
8888

8989
// Wait for message to send, get its ID and save as @threadId
90-
cy.get(".mx_RoomView_body .mx_EventTile").contains("Hello Mr. Bot")
91-
.closest(".mx_EventTile[data-scroll-tokens]").invoke("attr", "data-scroll-tokens").as("threadId");
90+
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
91+
.invoke("attr", "data-scroll-tokens").as("threadId");
9292

9393
// Bot starts thread
9494
cy.get<string>("@threadId").then(threadId => {
@@ -111,15 +111,15 @@ describe("Threads", () => {
111111
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
112112

113113
// User reacts to message instead
114-
cy.get(".mx_ThreadView .mx_EventTile").contains("Hello there").closest(".mx_EventTile_line")
114+
cy.get(".mx_ThreadView .mx_EventTile").contains(".mx_EventTile_line", "Hello there")
115115
.find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
116116
cy.get(".mx_EmojiPicker").within(() => {
117117
cy.get('input[type="text"]').type("wave");
118118
cy.get('[role="menuitem"]').contains("👋").click();
119119
});
120120

121121
// User redacts their prior response
122-
cy.get(".mx_ThreadView .mx_EventTile").contains("Test").closest(".mx_EventTile_line")
122+
cy.get(".mx_ThreadView .mx_EventTile").contains(".mx_EventTile_line", "Test")
123123
.find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
124124
cy.get(".mx_IconizedContextMenu").within(() => {
125125
cy.get('[role="menuitem"]').contains("Remove").click();
@@ -166,7 +166,7 @@ describe("Threads", () => {
166166
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Great!");
167167

168168
// User edits & asserts
169-
cy.get(".mx_ThreadView .mx_EventTile_last").contains("Great!").closest(".mx_EventTile_line").within(() => {
169+
cy.get(".mx_ThreadView .mx_EventTile_last").contains(".mx_EventTile_line", "Great!").within(() => {
170170
cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
171171
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
172172
});
+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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 type { MatrixClient } from "matrix-js-sdk/src/client";
20+
import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
21+
import { SynapseInstance } from "../../plugins/synapsedocker";
22+
import Chainable = Cypress.Chainable;
23+
import { UserCredentials } from "../../support/login";
24+
25+
function openSpaceCreateMenu(): Chainable<JQuery> {
26+
cy.get(".mx_SpaceButton_new").click();
27+
return cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu");
28+
}
29+
30+
function getSpacePanelButton(spaceName: string): Chainable<JQuery> {
31+
return cy.get(`.mx_SpaceButton[aria-label="${spaceName}"]`);
32+
}
33+
34+
function openSpaceContextMenu(spaceName: string): Chainable<JQuery> {
35+
getSpacePanelButton(spaceName).rightclick();
36+
return cy.get(".mx_SpacePanel_contextMenu");
37+
}
38+
39+
function spaceCreateOptions(spaceName: string): ICreateRoomOpts {
40+
return {
41+
creation_content: {
42+
type: "m.space",
43+
},
44+
initial_state: [{
45+
type: "m.room.name",
46+
content: {
47+
name: spaceName,
48+
},
49+
}],
50+
};
51+
}
52+
53+
function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
54+
return {
55+
type: "m.space.child",
56+
state_key: roomId,
57+
content: {
58+
via: [roomId.split(":")[1]],
59+
},
60+
};
61+
}
62+
63+
describe("Spaces", () => {
64+
let synapse: SynapseInstance;
65+
let user: UserCredentials;
66+
67+
beforeEach(() => {
68+
cy.startSynapse("default").then(data => {
69+
synapse = data;
70+
71+
cy.initTestUser(synapse, "Sue").then(_user => {
72+
user = _user;
73+
cy.mockClipboard();
74+
});
75+
});
76+
});
77+
78+
afterEach(() => {
79+
cy.stopSynapse(synapse);
80+
});
81+
82+
it("should allow user to create public space", () => {
83+
openSpaceCreateMenu().within(() => {
84+
cy.get(".mx_SpaceCreateMenuType_public").click();
85+
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
86+
.selectFile("cypress/fixtures/riot.png", { force: true });
87+
cy.get('input[label="Name"]').type("Let's have a Riot");
88+
cy.get('input[label="Address"]').should("have.value", "lets-have-a-riot");
89+
cy.get('textarea[label="Description"]').type("This is a space to reminisce Riot.im!");
90+
cy.get(".mx_AccessibleButton").contains("Create").click();
91+
});
92+
93+
// Create the default General & Random rooms, as well as a custom "Jokes" room
94+
cy.get('input[label="Room name"][value="General"]').should("exist");
95+
cy.get('input[label="Room name"][value="Random"]').should("exist");
96+
cy.get('input[placeholder="Support"]').type("Jokes");
97+
cy.get(".mx_AccessibleButton").contains("Continue").click();
98+
99+
// Copy matrix.to link
100+
cy.get(".mx_SpacePublicShare_shareButton").focus().realClick();
101+
cy.getClipboardText().should("eq", "https://matrix.to/#/#lets-have-a-riot:localhost");
102+
103+
// Go to space home
104+
cy.get(".mx_AccessibleButton").contains("Go to my first room").click();
105+
106+
// Assert rooms exist in the room list
107+
cy.get(".mx_RoomTile").contains("General").should("exist");
108+
cy.get(".mx_RoomTile").contains("Random").should("exist");
109+
cy.get(".mx_RoomTile").contains("Jokes").should("exist");
110+
});
111+
112+
it("should allow user to create private space", () => {
113+
openSpaceCreateMenu().within(() => {
114+
cy.get(".mx_SpaceCreateMenuType_private").click();
115+
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
116+
.selectFile("cypress/fixtures/riot.png", { force: true });
117+
cy.get('input[label="Name"]').type("This is not a Riot");
118+
cy.get('input[label="Address"]').should("not.exist");
119+
cy.get('textarea[label="Description"]').type("This is a private space of mourning Riot.im...");
120+
cy.get(".mx_AccessibleButton").contains("Create").click();
121+
});
122+
123+
cy.get(".mx_SpaceRoomView_privateScope_meAndMyTeammatesButton").click();
124+
125+
// Create the default General & Random rooms, as well as a custom "Projects" room
126+
cy.get('input[label="Room name"][value="General"]').should("exist");
127+
cy.get('input[label="Room name"][value="Random"]').should("exist");
128+
cy.get('input[placeholder="Support"]').type("Projects");
129+
cy.get(".mx_AccessibleButton").contains("Continue").click();
130+
131+
cy.get(".mx_SpaceRoomView").should("contain", "Invite your teammates");
132+
cy.get(".mx_AccessibleButton").contains("Skip for now").click();
133+
134+
// Assert rooms exist in the room list
135+
cy.get(".mx_RoomTile").contains("General").should("exist");
136+
cy.get(".mx_RoomTile").contains("Random").should("exist");
137+
cy.get(".mx_RoomTile").contains("Projects").should("exist");
138+
139+
// Assert rooms exist in the space explorer
140+
cy.get(".mx_SpaceHierarchy_roomTile").contains("General").should("exist");
141+
cy.get(".mx_SpaceHierarchy_roomTile").contains("Random").should("exist");
142+
cy.get(".mx_SpaceHierarchy_roomTile").contains("Projects").should("exist");
143+
});
144+
145+
it("should allow user to create just-me space", () => {
146+
cy.createRoom({
147+
name: "Sample Room",
148+
});
149+
150+
openSpaceCreateMenu().within(() => {
151+
cy.get(".mx_SpaceCreateMenuType_private").click();
152+
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
153+
.selectFile("cypress/fixtures/riot.png", { force: true });
154+
cy.get('input[label="Address"]').should("not.exist");
155+
cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im...");
156+
cy.get('input[label="Name"]').type("This is my Riot{enter}");
157+
});
158+
159+
cy.get(".mx_SpaceRoomView_privateScope_justMeButton").click();
160+
161+
cy.get(".mx_AddExistingToSpace_entry").click();
162+
cy.get(".mx_AccessibleButton").contains("Add").click();
163+
164+
cy.get(".mx_RoomTile").contains("Sample Room").should("exist");
165+
cy.get(".mx_SpaceHierarchy_roomTile").contains("Sample Room").should("exist");
166+
});
167+
168+
it("should allow user to invite another to a space", () => {
169+
let bot: MatrixClient;
170+
cy.getBot(synapse, "BotBob").then(_bot => {
171+
bot = _bot;
172+
});
173+
174+
cy.createSpace({
175+
visibility: "public" as any,
176+
room_alias_name: "space",
177+
}).as("spaceId");
178+
179+
openSpaceContextMenu("#space:localhost").within(() => {
180+
cy.get('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]').click();
181+
});
182+
183+
cy.get(".mx_SpacePublicShare").within(() => {
184+
// Copy link first
185+
cy.get(".mx_SpacePublicShare_shareButton").focus().realClick();
186+
cy.getClipboardText().should("eq", "https://matrix.to/#/#space:localhost");
187+
// Start Matrix invite flow
188+
cy.get(".mx_SpacePublicShare_inviteButton").click();
189+
});
190+
191+
cy.get(".mx_InviteDialog_other").within(() => {
192+
cy.get('input[type="text"]').type(bot.getUserId());
193+
cy.get(".mx_AccessibleButton").contains("Invite").click();
194+
});
195+
196+
cy.get(".mx_InviteDialog_other").should("not.exist");
197+
});
198+
199+
it("should show space invites at the top of the space panel", () => {
200+
cy.createSpace({
201+
name: "My Space",
202+
});
203+
getSpacePanelButton("My Space").should("exist");
204+
205+
cy.getBot(synapse, "BotBob").then({ timeout: 10000 }, async bot => {
206+
const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space"));
207+
await bot.invite(roomId, user.userId);
208+
});
209+
// Assert that `Space Space` is above `My Space` due to it being an invite
210+
getSpacePanelButton("Space Space").should("exist")
211+
.parent().next().find('.mx_SpaceButton[aria-label="My Space"]').should("exist");
212+
});
213+
214+
it("should include rooms in space home", () => {
215+
cy.createRoom({
216+
name: "Music",
217+
}).as("roomId1");
218+
cy.createRoom({
219+
name: "Gaming",
220+
}).as("roomId2");
221+
222+
const spaceName = "Spacey Mc. Space Space";
223+
cy.all([
224+
cy.get<string>("@roomId1"),
225+
cy.get<string>("@roomId2"),
226+
]).then(([roomId1, roomId2]) => {
227+
cy.createSpace({
228+
name: spaceName,
229+
initial_state: [
230+
spaceChildInitialState(roomId1),
231+
spaceChildInitialState(roomId2),
232+
],
233+
}).as("spaceId");
234+
});
235+
236+
cy.get("@spaceId").then(() => {
237+
getSpacePanelButton(spaceName).dblclick(); // Open space home
238+
});
239+
cy.get(".mx_SpaceRoomView .mx_SpaceHierarchy_list").within(() => {
240+
cy.get(".mx_SpaceHierarchy_roomTile").contains("Music").should("exist");
241+
cy.get(".mx_SpaceHierarchy_roomTile").contains("Gaming").should("exist");
242+
});
243+
});
244+
});

cypress/plugins/synapsedocker/index.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,6 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
6666
}
6767
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-'));
6868

69-
// change permissions on the temp directory so the docker container can see its contents
70-
await fse.chmod(tempDir, 0o777);
71-
7269
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
7370
console.log(`Copy ${templateDir} -> ${tempDir}`);
7471
await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' });
@@ -113,6 +110,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
113110
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
114111

115112
const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`;
113+
const userInfo = os.userInfo();
116114

117115
const synapseId = await new Promise<string>((resolve, reject) => {
118116
childProcess.execFile('docker', [
@@ -121,6 +119,8 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
121119
"-d",
122120
"-v", `${synCfg.configDir}:/data`,
123121
"-p", `${synCfg.port}:8008/tcp`,
122+
// We run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult
123+
"-u", `${userInfo.uid}:${userInfo.gid}`,
124124
"matrixdotorg/synapse:develop",
125125
"run",
126126
], (err, stdout) => {
@@ -129,8 +129,6 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
129129
});
130130
});
131131

132-
synapses.set(synapseId, { synapseId, ...synCfg });
133-
134132
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
135133

136134
// Await Synapse healthcheck
@@ -150,7 +148,9 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
150148
});
151149
});
152150

153-
return synapses.get(synapseId);
151+
const synapse: SynapseInstance = { synapseId, ...synCfg };
152+
synapses.set(synapseId, synapse);
153+
return synapse;
154154
}
155155

156156
async function synapseStop(id: string): Promise<void> {

cypress/support/client.ts

+15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ declare global {
3535
* @return the ID of the newly created room
3636
*/
3737
createRoom(options: ICreateRoomOpts): Chainable<string>;
38+
/**
39+
* Create a space with given options.
40+
* @param options the options to apply when creating the space
41+
* @return the ID of the newly created space (room)
42+
*/
43+
createSpace(options: ICreateRoomOpts): Chainable<string>;
3844
/**
3945
* Invites the given user to the given room.
4046
* @param roomId the id of the room to invite to
@@ -71,6 +77,15 @@ Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string>
7177
});
7278
});
7379

80+
Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable<string> => {
81+
return cy.createRoom({
82+
...options,
83+
creation_content: {
84+
"type": "m.space",
85+
},
86+
});
87+
});
88+
7489
Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => {
7590
return cy.getClient().then(async (cli: MatrixClient) => {
7691
return cli.invite(roomId, userId);

0 commit comments

Comments
 (0)