From 82981e4161dda0bb303855caf23ec706c74c5ec6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 13 Apr 2022 00:46:08 +0100 Subject: [PATCH] End to end tests for threads (#8267) --- test/end-to-end-tests/.gitignore | 1 + test/end-to-end-tests/src/rest/room.ts | 8 +- test/end-to-end-tests/src/scenario.ts | 9 ++ .../end-to-end-tests/src/scenarios/threads.ts | 83 ++++++++++ test/end-to-end-tests/src/session.ts | 7 +- .../src/usecases/rightpanel.ts | 22 +++ test/end-to-end-tests/src/usecases/signup.ts | 8 +- test/end-to-end-tests/src/usecases/threads.ts | 153 ++++++++++++++++++ 8 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 test/end-to-end-tests/src/scenarios/threads.ts create mode 100644 test/end-to-end-tests/src/usecases/threads.ts diff --git a/test/end-to-end-tests/.gitignore b/test/end-to-end-tests/.gitignore index 528c296f93..a604178f5f 100644 --- a/test/end-to-end-tests/.gitignore +++ b/test/end-to-end-tests/.gitignore @@ -4,3 +4,4 @@ element/env performance-entries.json lib logs +homeserver.log diff --git a/test/end-to-end-tests/src/rest/room.ts b/test/end-to-end-tests/src/rest/room.ts index da5f91a607..2261f95993 100644 --- a/test/end-to-end-tests/src/rest/room.ts +++ b/test/end-to-end-tests/src/rest/room.ts @@ -20,19 +20,19 @@ import uuidv4 = require('uuid/v4'); import { RestSession } from "./session"; import { Logger } from "../logger"; -/* no pun intented */ +/* no pun intended */ export class RestRoom { constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {} - async talk(message: string): Promise { + async talk(message: string): Promise { this.log.step(`says "${message}" in ${this.roomId}`); 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", "body": message, }); this.log.done(); - return txId; + return eventId; } async leave(): Promise { diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts index e6a588eac9..e843504c48 100644 --- a/test/end-to-end-tests/src/scenario.ts +++ b/test/end-to-end-tests/src/scenario.ts @@ -30,6 +30,8 @@ import { stickerScenarios } from './scenarios/sticker'; import { userViewScenarios } from "./scenarios/user-view"; import { ssoCustomisationScenarios } from "./scenarios/sso-customisations"; import { updateScenarios } from "./scenarios/update"; +import { threadsScenarios } from "./scenarios/threads"; +import { enableThreads } from "./usecases/threads"; export async function scenario(createSession: (s: string) => Promise, restCreator: RestSessionCreator): Promise { @@ -48,6 +50,12 @@ export async function scenario(createSession: (s: string) => Promise Promise { + 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 :)"); +} diff --git a/test/end-to-end-tests/src/session.ts b/test/end-to-end-tests/src/session.ts index 445cf1c477..86f612b0af 100644 --- a/test/end-to-end-tests/src/session.ts +++ b/test/end-to-end-tests/src/session.ts @@ -131,8 +131,11 @@ export class ElementSession { await input.type(text); } - public query(selector: string, timeout: number = DEFAULT_TIMEOUT, - hidden = false): Promise { + public query( + selector: string, + timeout: number = DEFAULT_TIMEOUT, + hidden = false, + ): Promise { return this.page.waitForSelector(selector, { visible: true, timeout, hidden }); } diff --git a/test/end-to-end-tests/src/usecases/rightpanel.ts b/test/end-to-end-tests/src/usecases/rightpanel.ts index c91e3fad57..83417ccb1a 100644 --- a/test/end-to-end-tests/src/usecases/rightpanel.ts +++ b/test/end-to-end-tests/src/usecases/rightpanel.ts @@ -16,6 +16,28 @@ limitations under the License. import { ElementSession } from "../session"; +export async function closeRoomRightPanel(session: ElementSession): Promise { + const button = await session.query(".mx_BaseCard_close"); + await button.click(); +} + +export async function openThreadListPanel(session: ElementSession): Promise { + 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 { + await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"] ' + + '.mx_RightPanel_headerButton_unreadIndicator'); +} + +export async function clickLatestThreadInThreadListPanel(session: ElementSession): Promise { + const threads = await session.queryAll(".mx_ThreadPanel .mx_EventTile"); + await threads[threads.length - 1].click(); +} + export async function openRoomRightPanel(session: ElementSession): Promise { // block until we have a roomSummaryButton const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]'); diff --git a/test/end-to-end-tests/src/usecases/signup.ts b/test/end-to-end-tests/src/usecases/signup.ts index 55301c3108..86d2720535 100644 --- a/test/end-to-end-tests/src/usecases/signup.ts +++ b/test/end-to-end-tests/src/usecases/signup.ts @@ -19,8 +19,12 @@ import { strict as assert } from 'assert'; import { ElementSession } from "../session"; -export async function signup(session: ElementSession, username: string, password: string, - homeserver: string): Promise { +export async function signup( + session: ElementSession, + username: string, + password: string, + homeserver: string, +): Promise { session.log.step("signs up"); await session.goto(session.url('/#/register')); // change the homeserver by clicking the advanced section diff --git a/test/end-to-end-tests/src/usecases/threads.ts b/test/end-to-end-tests/src/usecases/threads.ts new file mode 100644 index 0000000000..9e51fe7eec --- /dev/null +++ b/test/end-to-end-tests/src/usecases/threads.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); +}