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

Commit 82981e4

Browse files
authored
End to end tests for threads (#8267)
1 parent ecdc11d commit 82981e4

File tree

8 files changed

+283
-8
lines changed

8 files changed

+283
-8
lines changed

test/end-to-end-tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ element/env
44
performance-entries.json
55
lib
66
logs
7+
homeserver.log

test/end-to-end-tests/src/rest/room.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ import uuidv4 = require('uuid/v4');
2020
import { RestSession } from "./session";
2121
import { Logger } from "../logger";
2222

23-
/* no pun intented */
23+
/* no pun intended */
2424
export class RestRoom {
2525
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
2626

27-
async talk(message: string): Promise<void> {
27+
async talk(message: string): Promise<string> {
2828
this.log.step(`says "${message}" in ${this.roomId}`);
2929
const txId = uuidv4();
30-
await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
30+
const { event_id: eventId } = await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
3131
"msgtype": "m.text",
3232
"body": message,
3333
});
3434
this.log.done();
35-
return txId;
35+
return eventId;
3636
}
3737

3838
async leave(): Promise<void> {

test/end-to-end-tests/src/scenario.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { stickerScenarios } from './scenarios/sticker';
3030
import { userViewScenarios } from "./scenarios/user-view";
3131
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
3232
import { updateScenarios } from "./scenarios/update";
33+
import { threadsScenarios } from "./scenarios/threads";
34+
import { enableThreads } from "./usecases/threads";
3335

3436
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
3537
restCreator: RestSessionCreator): Promise<void> {
@@ -48,13 +50,20 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
4850
const alice = await createUser("alice");
4951
const bob = await createUser("bob");
5052

53+
// Enable threads for Alice & Bob before going any further as it requires refreshing the app
54+
// which otherwise loses all performance ticks.
55+
console.log("Enabling threads: ");
56+
await enableThreads(alice);
57+
await enableThreads(bob);
58+
5159
await toastScenarios(alice, bob);
5260
await userViewScenarios(alice, bob);
5361
await roomDirectoryScenarios(alice, bob);
5462
await e2eEncryptionScenarios(alice, bob);
5563
console.log("create REST users:");
5664
const charlies = await createRestUsers(restCreator);
5765
await lazyLoadingScenarios(alice, bob, charlies);
66+
await threadsScenarios(alice, bob);
5867
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
5968
await spacesScenarios(alice, bob);
6069

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 { ElementSession } from "../session";
18+
import {
19+
assertTimelineThreadSummary,
20+
clickTimelineThreadSummary,
21+
editThreadMessage,
22+
reactThreadMessage,
23+
redactThreadMessage,
24+
sendThreadMessage,
25+
startThread,
26+
} from "../usecases/threads";
27+
import { sendMessage } from "../usecases/send-message";
28+
import {
29+
assertThreadListHasUnreadIndicator,
30+
clickLatestThreadInThreadListPanel,
31+
closeRoomRightPanel,
32+
openThreadListPanel,
33+
} from "../usecases/rightpanel";
34+
35+
export async function threadsScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
36+
console.log(" threads tests:");
37+
38+
// Alice sends message
39+
await sendMessage(alice, "Hey bob, what do you think about X?");
40+
41+
// Bob responds via a thread
42+
await startThread(bob, "I think its Y!");
43+
44+
// Alice sees thread summary and opens thread panel
45+
await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
46+
await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
47+
await clickTimelineThreadSummary(alice);
48+
49+
// Bob closes right panel
50+
await closeRoomRightPanel(bob);
51+
52+
// Alice responds in thread
53+
await sendThreadMessage(alice, "Great!");
54+
await assertTimelineThreadSummary(alice, "alice", "Great!");
55+
await assertTimelineThreadSummary(bob, "alice", "Great!");
56+
57+
// Alice reacts to Bob's message instead
58+
await reactThreadMessage(alice, "😁");
59+
await assertTimelineThreadSummary(alice, "alice", "Great!");
60+
await assertTimelineThreadSummary(bob, "alice", "Great!");
61+
await redactThreadMessage(alice);
62+
await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
63+
await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
64+
65+
// Bob sees notification dot on the thread header icon
66+
await assertThreadListHasUnreadIndicator(bob);
67+
68+
// Bob opens thread list and inspects it
69+
await openThreadListPanel(bob);
70+
71+
// Bob opens thread in right panel via thread list
72+
await clickLatestThreadInThreadListPanel(bob);
73+
74+
// Bob responds to thread
75+
await sendThreadMessage(bob, "Testing threads s'more :)");
76+
await assertTimelineThreadSummary(alice, "bob", "Testing threads s'more :)");
77+
await assertTimelineThreadSummary(bob, "bob", "Testing threads s'more :)");
78+
79+
// Bob edits thread response
80+
await editThreadMessage(bob, "Testing threads some more :)");
81+
await assertTimelineThreadSummary(alice, "bob", "Testing threads some more :)");
82+
await assertTimelineThreadSummary(bob, "bob", "Testing threads some more :)");
83+
}

test/end-to-end-tests/src/session.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,11 @@ export class ElementSession {
131131
await input.type(text);
132132
}
133133

134-
public query(selector: string, timeout: number = DEFAULT_TIMEOUT,
135-
hidden = false): Promise<puppeteer.ElementHandle> {
134+
public query(
135+
selector: string,
136+
timeout: number = DEFAULT_TIMEOUT,
137+
hidden = false,
138+
): Promise<puppeteer.ElementHandle> {
136139
return this.page.waitForSelector(selector, { visible: true, timeout, hidden });
137140
}
138141

test/end-to-end-tests/src/usecases/rightpanel.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ limitations under the License.
1616

1717
import { ElementSession } from "../session";
1818

19+
export async function closeRoomRightPanel(session: ElementSession): Promise<void> {
20+
const button = await session.query(".mx_BaseCard_close");
21+
await button.click();
22+
}
23+
24+
export async function openThreadListPanel(session: ElementSession): Promise<void> {
25+
await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"]');
26+
const button = await session.queryWithoutWaiting('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"]' +
27+
':not(.mx_RightPanel_headerButton_highlight)');
28+
await button?.click();
29+
}
30+
31+
export async function assertThreadListHasUnreadIndicator(session: ElementSession): Promise<void> {
32+
await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"] ' +
33+
'.mx_RightPanel_headerButton_unreadIndicator');
34+
}
35+
36+
export async function clickLatestThreadInThreadListPanel(session: ElementSession): Promise<void> {
37+
const threads = await session.queryAll(".mx_ThreadPanel .mx_EventTile");
38+
await threads[threads.length - 1].click();
39+
}
40+
1941
export async function openRoomRightPanel(session: ElementSession): Promise<void> {
2042
// block until we have a roomSummaryButton
2143
const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');

test/end-to-end-tests/src/usecases/signup.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ import { strict as assert } from 'assert';
1919

2020
import { ElementSession } from "../session";
2121

22-
export async function signup(session: ElementSession, username: string, password: string,
23-
homeserver: string): Promise<void> {
22+
export async function signup(
23+
session: ElementSession,
24+
username: string,
25+
password: string,
26+
homeserver: string,
27+
): Promise<void> {
2428
session.log.step("signs up");
2529
await session.goto(session.url('/#/register'));
2630
// change the homeserver by clicking the advanced section
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 { strict as assert } from "assert";
18+
19+
import { ElementSession } from "../session";
20+
21+
export async function enableThreads(session: ElementSession): Promise<void> {
22+
session.log.step(`enables threads`);
23+
await session.page.evaluate(() => {
24+
window.localStorage.setItem("mx_seen_feature_thread_experimental", "1"); // inhibit dialog
25+
window["mxSettingsStore"].setValue("feature_thread", null, "device", true);
26+
});
27+
session.log.done();
28+
}
29+
30+
async function clickReplyInThread(session: ElementSession): Promise<void> {
31+
const events = await session.queryAll(".mx_EventTile_line");
32+
const event = events[events.length - 1];
33+
await event.hover();
34+
const button = await event.$(".mx_MessageActionBar_threadButton");
35+
await button.click();
36+
}
37+
38+
export async function sendThreadMessage(session: ElementSession, message: string): Promise<void> {
39+
session.log.step(`sends thread response "${message}"`);
40+
const composer = await session.query(".mx_ThreadView .mx_BasicMessageComposer_input");
41+
await composer.click();
42+
await composer.type(message);
43+
44+
const text = await session.innerText(composer);
45+
assert.equal(text.trim(), message.trim());
46+
await composer.press("Enter");
47+
// wait for the message to appear sent
48+
await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
49+
session.log.done();
50+
}
51+
52+
export async function editThreadMessage(session: ElementSession, message: string): Promise<void> {
53+
session.log.step(`edits thread response "${message}"`);
54+
const events = await session.queryAll(".mx_EventTile_line");
55+
const event = events[events.length - 1];
56+
await event.hover();
57+
const button = await event.$(".mx_MessageActionBar_editButton");
58+
await button.click();
59+
60+
const composer = await session.query(".mx_ThreadView .mx_EditMessageComposer .mx_BasicMessageComposer_input");
61+
await composer.click({ clickCount: 3 });
62+
await composer.type(message);
63+
64+
const text = await session.innerText(composer);
65+
assert.equal(text.trim(), message.trim());
66+
await composer.press("Enter");
67+
// wait for the edit to appear sent
68+
await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
69+
session.log.done();
70+
}
71+
72+
export async function redactThreadMessage(session: ElementSession): Promise<void> {
73+
session.log.startGroup(`redacts latest thread response`);
74+
75+
const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
76+
const event = events[events.length - 1];
77+
await event.hover();
78+
79+
session.log.step(`clicks the ... button`);
80+
let button = await event.$('.mx_MessageActionBar [aria-label="Options"]');
81+
await button.click();
82+
session.log.done();
83+
84+
session.log.step(`clicks the remove option`);
85+
button = await session.query('.mx_IconizedContextMenu_item[aria-label="Remove"]');
86+
await button.click();
87+
session.log.done();
88+
89+
session.log.step(`confirms in the dialog`);
90+
button = await session.query(".mx_Dialog_primary");
91+
await button.click();
92+
session.log.done();
93+
94+
await session.query(".mx_ThreadView .mx_RedactedBody");
95+
96+
session.log.endGroup();
97+
}
98+
99+
export async function reactThreadMessage(session: ElementSession, reaction: string): Promise<void> {
100+
session.log.startGroup(`reacts to latest thread response`);
101+
102+
const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
103+
const event = events[events.length - 1];
104+
await event.hover();
105+
106+
session.log.step(`clicks the reaction button`);
107+
let button = await event.$('.mx_MessageActionBar [aria-label="React"]');
108+
await button.click();
109+
session.log.done();
110+
111+
session.log.step(`selects reaction`);
112+
button = await session.query(`.mx_EmojiPicker_item_wrapper[aria-label=${reaction}]`);
113+
await button.click;
114+
session.log.done();
115+
116+
session.log.step(`clicks away`);
117+
button = await session.query(".mx_ContextualMenu_background");
118+
await button.click();
119+
session.log.done();
120+
121+
session.log.endGroup();
122+
}
123+
124+
export async function startThread(session: ElementSession, response: string): Promise<void> {
125+
session.log.startGroup(`creates thread on latest message`);
126+
127+
await clickReplyInThread(session);
128+
await sendThreadMessage(session, response);
129+
130+
session.log.endGroup();
131+
}
132+
133+
export async function assertTimelineThreadSummary(
134+
session: ElementSession,
135+
sender: string,
136+
content: string,
137+
): Promise<void> {
138+
session.log.step("asserts the timeline thread summary is as expected");
139+
const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadInfo");
140+
const summary = summaries[summaries.length - 1];
141+
assert.equal(await session.innerText(await summary.$(".mx_ThreadInfo_sender")), sender);
142+
assert.equal(await session.innerText(await summary.$(".mx_ThreadInfo_content")), content);
143+
session.log.done();
144+
}
145+
146+
export async function clickTimelineThreadSummary(session: ElementSession): Promise<void> {
147+
session.log.step(`clicks the latest thread summary in the timeline`);
148+
149+
const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadInfo");
150+
await summaries[summaries.length - 1].click();
151+
152+
session.log.done();
153+
}

0 commit comments

Comments
 (0)