From beafe686a98b65076f8fbaee4741ba65458beab9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Aug 2023 15:08:53 +0100 Subject: [PATCH] Wire up more high level read receipt tests (#11408) * Improve existing tests * Wire up more tests * Wire up more tests * Wire up more tests * Wire up more tests --- cypress/e2e/read-receipts/high-level.spec.ts | 266 ++++++++++++++++--- 1 file changed, 222 insertions(+), 44 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 7af0daeb9e..bd61e62d05 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -68,11 +68,15 @@ describe("Read receipts", () => { cy.stopHomeserver(homeserver); }); - abstract class MessageSpec { + abstract class MessageContentSpec { public abstract getContent(room: Room): Promise>; } - type Message = string | MessageSpec; + abstract class BotActionSpec { + public abstract performAction(cli: MatrixClient, room: Room): Promise; + } + + type Message = string | MessageContentSpec | BotActionSpec; function goTo(room: string) { cy.viewRoomByName(room); @@ -95,24 +99,39 @@ describe("Read receipts", () => { cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1); } - /** - * Sends messages into given room as a bot - * @param room - the name of the room to send messages into - * @param messages - the list of messages to send, these can be strings or implementations of MessageSpace like `editOf` - */ - function receiveMessages(room: string, messages: Message[]) { + function sendMessageAsClient(cli: MatrixClient, room: string, messages: Message[]) { findRoomByName(room).then(async ({ roomId }) => { - const room = bot.getRoom(roomId); + const room = cli.getRoom(roomId); for (const message of messages) { if (typeof message === "string") { - await bot.sendTextMessage(roomId, message); + await cli.sendTextMessage(roomId, message); + } else if (message instanceof MessageContentSpec) { + await cli.sendMessage(roomId, await message.getContent(room)); } else { - await bot.sendMessage(roomId, await message.getContent(room)); + await message.performAction(cli, room); } } }); } + /** + * Sends messages into given room as a bot + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + function receiveMessages(room: string, messages: Message[]) { + sendMessageAsClient(bot, room, messages); + } + + /** + * Sends messages into given room as the currently logged-in user + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + function sendMessages(room: string, messages: Message[]) { + cy.getClient().then((cli) => sendMessageAsClient(cli, room, messages)); + } + /** * Utility to find a MatrixEvent by its body content * @param room - the room to search for the event in @@ -140,12 +159,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send an edit into a room + * MessageContentSpec to send an edit into a room * @param originalMessage - the body of the message to edit * @param newMessage - the message body to send in the edit */ - function editOf(originalMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function editOf(originalMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, originalMessage, true); @@ -163,12 +182,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send a reply into a room + * MessageContentSpec to send a reply into a room * @param targetMessage - the body of the message to reply to * @param newMessage - the message body to send into the reply */ - function replyTo(targetMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function replyTo(targetMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, targetMessage); @@ -186,12 +205,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send a threaded response into a room + * MessageContentSpec to send a threaded response into a room * @param rootMessage - the body of the thread root message to send a response to * @param newMessage - the message body to send into the thread response */ - function threadedOff(rootMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, rootMessage); @@ -208,6 +227,53 @@ describe("Read receipts", () => { })(); } + /** + * BotActionSpec to send a reaction to an existing event into a room + * @param targetMessage - the body of the message to send a reaction to + * @param reaction - the key of the reaction to send into the room + */ + function reactionTo(targetMessage: string, reaction: string): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + const ev = await getMessage(room, targetMessage, true); + const threadId = !ev.isThreadRoot ? ev.threadRootId : undefined; + await cli.sendEvent(room.roomId, threadId ?? null, "m.reaction", { + "m.relates_to": { + rel_type: "m.annotation", + event_id: ev.getId(), + key: reaction, + }, + }); + } + })(); + } + + /** + * BotActionSpec to send a custom event + * @param eventType - the type of the event to send + * @param content - the event content to send + */ + function customEvent(eventType: string, content: Record): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + await cli.sendEvent(room.roomId, null, eventType, content); + } + })(); + } + + /** + * BotActionSpec to send a redaction into a room + * @param targetMessage - the body of the message to send a redaction to + */ + function redactionOf(targetMessage: string): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + const ev = await getMessage(room, targetMessage, true); + await cli.redactEvent(room.roomId, ev.threadRootId, ev.getId()); + } + })(); + } + function getRoomListTile(room: string) { return cy.findByRole("treeitem", { name: new RegExp("^" + room), log: false }); } @@ -246,7 +312,7 @@ describe("Read receipts", () => { cy.log("Open thread list"); cy.findByTestId("threadsButton", { log: false }).then(($button) => { if ($button?.attr("aria-current") !== "true") { - $button.trigger("click"); + cy.findByTestId("threadsButton", { log: false }).click(); } }); @@ -296,7 +362,7 @@ describe("Read receipts", () => { describe("new messages", () => { describe("in the main timeline", () => { - it("Sending a message makes a room unread", () => { + it("Receiving a message makes a room unread", () => { goTo(room1); assertRead(room2); @@ -323,7 +389,7 @@ describe("Read receipts", () => { markAsRead(room2); assertRead(room2); }); - it("Sending a new message after marking as read makes it unread", () => { + it("Receiving a new message after marking as read makes it unread", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); @@ -356,6 +422,16 @@ describe("Read receipts", () => { saveAndReload(); assertRead(room2); }); + it.skip("Sending a message from a different client marks room as read", () => { + goTo(room1); + assertRead(room2); + + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + sendMessages(room2, ["Msg2"]); + assertRead(room2); + }); }); describe("in threads", () => { @@ -527,7 +603,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); // (Sanity) + assertUnread(room2, 2); // (Sanity) // When I read the main timeline goTo(room2); @@ -801,24 +877,78 @@ describe("Read receipts", () => { }); describe("reactions", () => { - // Justification for this section: edits an reactions are similar, so we - // might choose to miss this section, but I have included it because - // edits replace the content of the original event in our code and - // reactions don't, so it seems possible that bugs could creep in that - // affect only one or the other. - describe("in the main timeline", () => { - it.skip("Reacting to a message makes a room unread", () => {}); - it.skip("Reading a reaction makes the room read", () => {}); - it.skip("Marking a room as read after a reaction makes it read", () => {}); - it.skip("Reacting to a message after marking as read makes the room unread", () => {}); - it.skip("A room with a reaction is still unread after restart", () => {}); - it.skip("A room where all reactions are read is still read after restart", () => {}); + it("Receiving a reaction to a message does not make a room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + }); + it("Reacting to a message after marking as read does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + }); + it("A room with an unread reaction is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); + it("A room where all reactions are read is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2", reactionTo("Msg2", "🪿")]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); }); describe("in threads", () => { - it.skip("A reaction to a threaded message makes the room unread", () => {}); - it.skip("Reading a reaction to a threaded message makes the room read", () => {}); + it("A reaction to a threaded message does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Reply1", "🪿")]); + + assertRead(room2); + }); it.skip("Marking a room as read after a reaction in a thread makes it read", () => {}); it.skip("Reacting to a thread message after marking as read makes the room unread", () => {}); it.skip("A room with a reaction to a threaded message is still unread after restart", () => {}); @@ -826,7 +956,21 @@ describe("Read receipts", () => { }); describe("thread roots", () => { - it.skip("A reaction to a thread root makes the room unread", () => {}); + it("A reaction to a thread root does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Msg1", "🪿")]); + + assertRead(room2); + }); it.skip("Reading a reaction to a thread root makes the room read", () => {}); it.skip("Marking a room as read after a reaction to a thread root makes it read", () => {}); it.skip("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {}); @@ -835,9 +979,20 @@ describe("Read receipts", () => { describe("redactions", () => { describe("in the main timeline", () => { - // One of the following two must be right: - it.skip("Redacting the message pointed to by my receipt leaves the room read", () => {}); - it.skip("Redacting a message after it was read makes the room unread", () => {}); + it("Redacting the message pointed to by my receipt leaves the room read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 1); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [redactionOf("Msg2")]); + assertRead(room2); + }); it.skip("Reading an unread room after a redaction of the latest message makes it read", () => {}); it.skip("Reading an unread room after a redaction of an older message makes it read", () => {}); @@ -949,8 +1104,31 @@ describe("Read receipts", () => { }); describe("Ignored events", () => { - it.skip("If all events after receipt are unimportant, the room is read", () => {}); - it.skip("Sending an important event after unimportant ones makes the room unread", () => {}); + it("If all events after receipt are unimportant, the room is read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + + receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]); + assertRead(room2); + }); + it("Sending an important event after unimportant ones makes the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + + receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]); + assertRead(room2); + + receiveMessages(room2, ["Hello"]); + assertUnread(room2, 1); + }); it.skip("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); });