End to end tests for threads (#8267)
parent
ecdc11d3d5
commit
82981e4161
|
@ -4,3 +4,4 @@ element/env
|
||||||
performance-entries.json
|
performance-entries.json
|
||||||
lib
|
lib
|
||||||
logs
|
logs
|
||||||
|
homeserver.log
|
||||||
|
|
|
@ -20,19 +20,19 @@ import uuidv4 = require('uuid/v4');
|
||||||
import { RestSession } from "./session";
|
import { RestSession } from "./session";
|
||||||
import { Logger } from "../logger";
|
import { Logger } from "../logger";
|
||||||
|
|
||||||
/* no pun intented */
|
/* no pun intended */
|
||||||
export class RestRoom {
|
export class RestRoom {
|
||||||
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
|
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
|
||||||
|
|
||||||
async talk(message: string): Promise<void> {
|
async talk(message: string): Promise<string> {
|
||||||
this.log.step(`says "${message}" in ${this.roomId}`);
|
this.log.step(`says "${message}" in ${this.roomId}`);
|
||||||
const txId = uuidv4();
|
const txId = uuidv4();
|
||||||
await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
|
const { event_id: eventId } = await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
"body": message,
|
"body": message,
|
||||||
});
|
});
|
||||||
this.log.done();
|
this.log.done();
|
||||||
return txId;
|
return eventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async leave(): Promise<void> {
|
async leave(): Promise<void> {
|
||||||
|
|
|
@ -30,6 +30,8 @@ import { stickerScenarios } from './scenarios/sticker';
|
||||||
import { userViewScenarios } from "./scenarios/user-view";
|
import { userViewScenarios } from "./scenarios/user-view";
|
||||||
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
|
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
|
||||||
import { updateScenarios } from "./scenarios/update";
|
import { updateScenarios } from "./scenarios/update";
|
||||||
|
import { threadsScenarios } from "./scenarios/threads";
|
||||||
|
import { enableThreads } from "./usecases/threads";
|
||||||
|
|
||||||
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
||||||
restCreator: RestSessionCreator): Promise<void> {
|
restCreator: RestSessionCreator): Promise<void> {
|
||||||
|
@ -48,6 +50,12 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
const alice = await createUser("alice");
|
const alice = await createUser("alice");
|
||||||
const bob = await createUser("bob");
|
const bob = await createUser("bob");
|
||||||
|
|
||||||
|
// Enable threads for Alice & Bob before going any further as it requires refreshing the app
|
||||||
|
// which otherwise loses all performance ticks.
|
||||||
|
console.log("Enabling threads: ");
|
||||||
|
await enableThreads(alice);
|
||||||
|
await enableThreads(bob);
|
||||||
|
|
||||||
await toastScenarios(alice, bob);
|
await toastScenarios(alice, bob);
|
||||||
await userViewScenarios(alice, bob);
|
await userViewScenarios(alice, bob);
|
||||||
await roomDirectoryScenarios(alice, bob);
|
await roomDirectoryScenarios(alice, bob);
|
||||||
|
@ -55,6 +63,7 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
console.log("create REST users:");
|
console.log("create REST users:");
|
||||||
const charlies = await createRestUsers(restCreator);
|
const charlies = await createRestUsers(restCreator);
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
await lazyLoadingScenarios(alice, bob, charlies);
|
||||||
|
await threadsScenarios(alice, bob);
|
||||||
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
|
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
|
||||||
await spacesScenarios(alice, bob);
|
await spacesScenarios(alice, bob);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
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 { ElementSession } from "../session";
|
||||||
|
import {
|
||||||
|
assertTimelineThreadSummary,
|
||||||
|
clickTimelineThreadSummary,
|
||||||
|
editThreadMessage,
|
||||||
|
reactThreadMessage,
|
||||||
|
redactThreadMessage,
|
||||||
|
sendThreadMessage,
|
||||||
|
startThread,
|
||||||
|
} from "../usecases/threads";
|
||||||
|
import { sendMessage } from "../usecases/send-message";
|
||||||
|
import {
|
||||||
|
assertThreadListHasUnreadIndicator,
|
||||||
|
clickLatestThreadInThreadListPanel,
|
||||||
|
closeRoomRightPanel,
|
||||||
|
openThreadListPanel,
|
||||||
|
} from "../usecases/rightpanel";
|
||||||
|
|
||||||
|
export async function threadsScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
|
||||||
|
console.log(" threads tests:");
|
||||||
|
|
||||||
|
// Alice sends message
|
||||||
|
await sendMessage(alice, "Hey bob, what do you think about X?");
|
||||||
|
|
||||||
|
// Bob responds via a thread
|
||||||
|
await startThread(bob, "I think its Y!");
|
||||||
|
|
||||||
|
// Alice sees thread summary and opens thread panel
|
||||||
|
await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
|
||||||
|
await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
|
||||||
|
await clickTimelineThreadSummary(alice);
|
||||||
|
|
||||||
|
// Bob closes right panel
|
||||||
|
await closeRoomRightPanel(bob);
|
||||||
|
|
||||||
|
// Alice responds in thread
|
||||||
|
await sendThreadMessage(alice, "Great!");
|
||||||
|
await assertTimelineThreadSummary(alice, "alice", "Great!");
|
||||||
|
await assertTimelineThreadSummary(bob, "alice", "Great!");
|
||||||
|
|
||||||
|
// Alice reacts to Bob's message instead
|
||||||
|
await reactThreadMessage(alice, "😁");
|
||||||
|
await assertTimelineThreadSummary(alice, "alice", "Great!");
|
||||||
|
await assertTimelineThreadSummary(bob, "alice", "Great!");
|
||||||
|
await redactThreadMessage(alice);
|
||||||
|
await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
|
||||||
|
await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
|
||||||
|
|
||||||
|
// Bob sees notification dot on the thread header icon
|
||||||
|
await assertThreadListHasUnreadIndicator(bob);
|
||||||
|
|
||||||
|
// Bob opens thread list and inspects it
|
||||||
|
await openThreadListPanel(bob);
|
||||||
|
|
||||||
|
// Bob opens thread in right panel via thread list
|
||||||
|
await clickLatestThreadInThreadListPanel(bob);
|
||||||
|
|
||||||
|
// Bob responds to thread
|
||||||
|
await sendThreadMessage(bob, "Testing threads s'more :)");
|
||||||
|
await assertTimelineThreadSummary(alice, "bob", "Testing threads s'more :)");
|
||||||
|
await assertTimelineThreadSummary(bob, "bob", "Testing threads s'more :)");
|
||||||
|
|
||||||
|
// Bob edits thread response
|
||||||
|
await editThreadMessage(bob, "Testing threads some more :)");
|
||||||
|
await assertTimelineThreadSummary(alice, "bob", "Testing threads some more :)");
|
||||||
|
await assertTimelineThreadSummary(bob, "bob", "Testing threads some more :)");
|
||||||
|
}
|
|
@ -131,8 +131,11 @@ export class ElementSession {
|
||||||
await input.type(text);
|
await input.type(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public query(selector: string, timeout: number = DEFAULT_TIMEOUT,
|
public query(
|
||||||
hidden = false): Promise<puppeteer.ElementHandle> {
|
selector: string,
|
||||||
|
timeout: number = DEFAULT_TIMEOUT,
|
||||||
|
hidden = false,
|
||||||
|
): Promise<puppeteer.ElementHandle> {
|
||||||
return this.page.waitForSelector(selector, { visible: true, timeout, hidden });
|
return this.page.waitForSelector(selector, { visible: true, timeout, hidden });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,28 @@ limitations under the License.
|
||||||
|
|
||||||
import { ElementSession } from "../session";
|
import { ElementSession } from "../session";
|
||||||
|
|
||||||
|
export async function closeRoomRightPanel(session: ElementSession): Promise<void> {
|
||||||
|
const button = await session.query(".mx_BaseCard_close");
|
||||||
|
await button.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openThreadListPanel(session: ElementSession): Promise<void> {
|
||||||
|
await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"]');
|
||||||
|
const button = await session.queryWithoutWaiting('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"]' +
|
||||||
|
':not(.mx_RightPanel_headerButton_highlight)');
|
||||||
|
await button?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertThreadListHasUnreadIndicator(session: ElementSession): Promise<void> {
|
||||||
|
await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"] ' +
|
||||||
|
'.mx_RightPanel_headerButton_unreadIndicator');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickLatestThreadInThreadListPanel(session: ElementSession): Promise<void> {
|
||||||
|
const threads = await session.queryAll(".mx_ThreadPanel .mx_EventTile");
|
||||||
|
await threads[threads.length - 1].click();
|
||||||
|
}
|
||||||
|
|
||||||
export async function openRoomRightPanel(session: ElementSession): Promise<void> {
|
export async function openRoomRightPanel(session: ElementSession): Promise<void> {
|
||||||
// block until we have a roomSummaryButton
|
// block until we have a roomSummaryButton
|
||||||
const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');
|
const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');
|
||||||
|
|
|
@ -19,8 +19,12 @@ import { strict as assert } from 'assert';
|
||||||
|
|
||||||
import { ElementSession } from "../session";
|
import { ElementSession } from "../session";
|
||||||
|
|
||||||
export async function signup(session: ElementSession, username: string, password: string,
|
export async function signup(
|
||||||
homeserver: string): Promise<void> {
|
session: ElementSession,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
homeserver: string,
|
||||||
|
): Promise<void> {
|
||||||
session.log.step("signs up");
|
session.log.step("signs up");
|
||||||
await session.goto(session.url('/#/register'));
|
await session.goto(session.url('/#/register'));
|
||||||
// change the homeserver by clicking the advanced section
|
// change the homeserver by clicking the advanced section
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
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 { strict as assert } from "assert";
|
||||||
|
|
||||||
|
import { ElementSession } from "../session";
|
||||||
|
|
||||||
|
export async function enableThreads(session: ElementSession): Promise<void> {
|
||||||
|
session.log.step(`enables threads`);
|
||||||
|
await session.page.evaluate(() => {
|
||||||
|
window.localStorage.setItem("mx_seen_feature_thread_experimental", "1"); // inhibit dialog
|
||||||
|
window["mxSettingsStore"].setValue("feature_thread", null, "device", true);
|
||||||
|
});
|
||||||
|
session.log.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickReplyInThread(session: ElementSession): Promise<void> {
|
||||||
|
const events = await session.queryAll(".mx_EventTile_line");
|
||||||
|
const event = events[events.length - 1];
|
||||||
|
await event.hover();
|
||||||
|
const button = await event.$(".mx_MessageActionBar_threadButton");
|
||||||
|
await button.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendThreadMessage(session: ElementSession, message: string): Promise<void> {
|
||||||
|
session.log.step(`sends thread response "${message}"`);
|
||||||
|
const composer = await session.query(".mx_ThreadView .mx_BasicMessageComposer_input");
|
||||||
|
await composer.click();
|
||||||
|
await composer.type(message);
|
||||||
|
|
||||||
|
const text = await session.innerText(composer);
|
||||||
|
assert.equal(text.trim(), message.trim());
|
||||||
|
await composer.press("Enter");
|
||||||
|
// wait for the message to appear sent
|
||||||
|
await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
|
||||||
|
session.log.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editThreadMessage(session: ElementSession, message: string): Promise<void> {
|
||||||
|
session.log.step(`edits thread response "${message}"`);
|
||||||
|
const events = await session.queryAll(".mx_EventTile_line");
|
||||||
|
const event = events[events.length - 1];
|
||||||
|
await event.hover();
|
||||||
|
const button = await event.$(".mx_MessageActionBar_editButton");
|
||||||
|
await button.click();
|
||||||
|
|
||||||
|
const composer = await session.query(".mx_ThreadView .mx_EditMessageComposer .mx_BasicMessageComposer_input");
|
||||||
|
await composer.click({ clickCount: 3 });
|
||||||
|
await composer.type(message);
|
||||||
|
|
||||||
|
const text = await session.innerText(composer);
|
||||||
|
assert.equal(text.trim(), message.trim());
|
||||||
|
await composer.press("Enter");
|
||||||
|
// wait for the edit to appear sent
|
||||||
|
await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
|
||||||
|
session.log.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function redactThreadMessage(session: ElementSession): Promise<void> {
|
||||||
|
session.log.startGroup(`redacts latest thread response`);
|
||||||
|
|
||||||
|
const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
|
||||||
|
const event = events[events.length - 1];
|
||||||
|
await event.hover();
|
||||||
|
|
||||||
|
session.log.step(`clicks the ... button`);
|
||||||
|
let button = await event.$('.mx_MessageActionBar [aria-label="Options"]');
|
||||||
|
await button.click();
|
||||||
|
session.log.done();
|
||||||
|
|
||||||
|
session.log.step(`clicks the remove option`);
|
||||||
|
button = await session.query('.mx_IconizedContextMenu_item[aria-label="Remove"]');
|
||||||
|
await button.click();
|
||||||
|
session.log.done();
|
||||||
|
|
||||||
|
session.log.step(`confirms in the dialog`);
|
||||||
|
button = await session.query(".mx_Dialog_primary");
|
||||||
|
await button.click();
|
||||||
|
session.log.done();
|
||||||
|
|
||||||
|
await session.query(".mx_ThreadView .mx_RedactedBody");
|
||||||
|
|
||||||
|
session.log.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reactThreadMessage(session: ElementSession, reaction: string): Promise<void> {
|
||||||
|
session.log.startGroup(`reacts to latest thread response`);
|
||||||
|
|
||||||
|
const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
|
||||||
|
const event = events[events.length - 1];
|
||||||
|
await event.hover();
|
||||||
|
|
||||||
|
session.log.step(`clicks the reaction button`);
|
||||||
|
let button = await event.$('.mx_MessageActionBar [aria-label="React"]');
|
||||||
|
await button.click();
|
||||||
|
session.log.done();
|
||||||
|
|
||||||
|
session.log.step(`selects reaction`);
|
||||||
|
button = await session.query(`.mx_EmojiPicker_item_wrapper[aria-label=${reaction}]`);
|
||||||
|
await button.click;
|
||||||
|
session.log.done();
|
||||||
|
|
||||||
|
session.log.step(`clicks away`);
|
||||||
|
button = await session.query(".mx_ContextualMenu_background");
|
||||||
|
await button.click();
|
||||||
|
session.log.done();
|
||||||
|
|
||||||
|
session.log.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startThread(session: ElementSession, response: string): Promise<void> {
|
||||||
|
session.log.startGroup(`creates thread on latest message`);
|
||||||
|
|
||||||
|
await clickReplyInThread(session);
|
||||||
|
await sendThreadMessage(session, response);
|
||||||
|
|
||||||
|
session.log.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertTimelineThreadSummary(
|
||||||
|
session: ElementSession,
|
||||||
|
sender: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<void> {
|
||||||
|
session.log.step("asserts the timeline thread summary is as expected");
|
||||||
|
const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadInfo");
|
||||||
|
const summary = summaries[summaries.length - 1];
|
||||||
|
assert.equal(await session.innerText(await summary.$(".mx_ThreadInfo_sender")), sender);
|
||||||
|
assert.equal(await session.innerText(await summary.$(".mx_ThreadInfo_content")), content);
|
||||||
|
session.log.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickTimelineThreadSummary(session: ElementSession): Promise<void> {
|
||||||
|
session.log.step(`clicks the latest thread summary in the timeline`);
|
||||||
|
|
||||||
|
const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadInfo");
|
||||||
|
await summaries[summaries.length - 1].click();
|
||||||
|
|
||||||
|
session.log.done();
|
||||||
|
}
|
Loading…
Reference in New Issue