diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 0685c3af09c..bdec38a409c 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -14,6 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ +/* + * # High Level Read Receipt Tests + * + * Tips for writing these tests: + * + * * Break up your tests into the smallest test case possible. The purpose of + * these tests is to understand hard-to-find bugs, so small tests are necessary. + * We know that Cypress recommends combining tests together for performance, but + * that will frustrate our goals here. (We will need to find a different way to + * reduce CI time.) + * + * * Try to assert something after every action, to make sure it has completed. + * E.g.: + * markAsRead(room2); + * assertRead(room2); + * You should especially follow this rule if you are jumping to a different + * room or similar straight afterwards. + * + * * Use assertStillRead() if you are asserting something is read when it was + * also read before. This waits a little while to make sure you're not getting a + * false positive. + */ + /// import type { MatrixClient, MatrixEvent, Room, IndexedDBStore } from "matrix-js-sdk/src/matrix"; @@ -137,6 +160,16 @@ describe("Read receipts", () => { cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1); } + /** + * Close the threads panel. (Actually, close any right panel, but for these + * tests we only open the threads panel.) + */ + function closeThreadsPanel() { + cy.log("Close threads panel"); + cy.get(".mx_RightPanel").findByTitle("Close").click(); + cy.get(".mx_RightPanel").should("not.exist"); + } + function sendMessageAsClient(cli: MatrixClient, room: string, messages: Message[]) { findRoomByName(room).then(async ({ roomId }) => { const room = cli.getRoom(roomId); @@ -398,6 +431,11 @@ describe("Read receipts", () => { function openThreadList() { cy.log("Open threads list"); + + // If we've just entered the room, the threads panel takes a while to decide + // whether it's open or not - wait here to give it a chance to settle. + cy.wait(200); + cy.findByTestId("threadsButton", { log: false }).then(($button) => { if ($button?.attr("aria-current") !== "true") { cy.findByTestId("threadsButton", { log: false }).click(); @@ -408,7 +446,8 @@ describe("Read receipts", () => { .should("exist") .then(($panel) => { const $button = $panel.find('.mx_BaseCard_back[title="Threads"]'); - // If the Threads back button is present then click it, the threads button can open either threads list or thread panel + // If the Threads back button is present then click it - the + // threads button can open either threads list or thread panel if ($button.length) { $button.trigger("click"); } @@ -421,6 +460,7 @@ describe("Read receipts", () => { } function assertReadThread(rootMessage: string) { + cy.log("Assert thread read", rootMessage); return getThreadListTile(rootMessage).within(() => { cy.get(".mx_NotificationBadge", { log: false }).should("not.exist"); }); @@ -1281,24 +1321,108 @@ describe("Read receipts", () => { describe("in threads", () => { it("A reaction to a threaded message does not make the room unread", () => { + // Given a thread exists and I have read it goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); assertUnread(room2, 2); - goTo(room2); openThread("Msg1"); assertRead(room2); + goTo(room1); + + // When someone reacts to a thread message + receiveMessages(room2, [reactionTo("Reply1", "🪿")]); + + // Then the room remains read + assertStillRead(room2); + }); + // XXX: fails because the room is still "bold" even though the notification counts all disappear + it.skip("Marking a room as read after a reaction in a thread makes it read", () => { + // Given a thread exists with a reaction + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1"), reactionTo("Reply1", "🪿")]); + assertUnread(room2, 2); + + // When I mark the room as read + markAsRead(room2); + + // Then it becomes read + assertRead(room2); + }); + // XXX: fails because the room is still "bold" even though the notification counts all disappear + it.skip("Reacting to a thread message after marking as read does not make the room unread", () => { + // Given a thread exists and I have marked it as read + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1"), reactionTo("Reply1", "🪿")]); + assertUnread(room2, 2); + markAsRead(room2); + assertRead(room2); + + // When someone reacts to a thread message + receiveMessages(room2, [reactionTo("Reply1", "🪿")]); + // Then the room remains read + assertStillRead(room2); + }); + it.skip("A room with a reaction to a threaded message is still unread after restart", () => { + // Given a thread exists and I have read it goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + goTo(room2); + openThread("Msg1"); + assertRead(room2); + goTo(room1); + + // And someone reacted to it, which doesn't stop it being read receiveMessages(room2, [reactionTo("Reply1", "🪿")]); + assertStillRead(room2); + + // When I restart + saveAndReload(); + + // Then the room is still read + assertRead(room2); + }); + it("A room where all reactions in threads are read is still read after restart", () => { + // Given multiple threads with reactions exist and are read + goTo(room1); + assertRead(room2); + receiveMessages(room2, [ + "Msg1", + threadedOff("Msg1", "Reply1a"), + reactionTo("Reply1a", "r"), + "Msg2", + threadedOff("Msg1", "Reply1b"), + threadedOff("Msg2", "Reply2a"), + reactionTo("Msg1", "e"), + threadedOff("Msg2", "Reply2b"), + reactionTo("Reply2a", "a"), + reactionTo("Reply2b", "c"), + reactionTo("Reply1b", "t"), + ]); + assertUnread(room2, 6); + goTo(room2); + openThread("Msg1"); + assertReadThread("Msg1"); + openThread("Msg2"); + assertReadThread("Msg2"); + assertRead(room2); + goTo(room1); + + // When I restart + saveAndReload(); + // Then the room is still read assertRead(room2); + goTo(room2); + assertReadThread("Msg1"); + assertReadThread("Msg2"); }); - 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", () => {}); - it.skip("A room where all reactions in threads are read is still read after restart", () => {}); }); describe("thread roots", () => { @@ -1618,24 +1742,242 @@ describe("Read receipts", () => { }); describe("in threads", () => { - // One of the following two must be right: - it.skip("Redacting the threaded message pointed to by my receipt leaves the room read", () => {}); - it.skip("Redacting a threaded message after it was read makes the room unread", () => {}); + it("Redacting the threaded message pointed to by my receipt leaves the room read", () => { + // Given I have some threads + goTo(room1); + receiveMessages(room2, [ + "Root", + threadedOff("Root", "ThreadMsg1"), + threadedOff("Root", "ThreadMsg2"), + "Root2", + threadedOff("Root2", "Root2->A"), + ]); + assertUnread(room2, 5); + + // And I have read them + goTo(room2); + assertUnreadThread("Root"); + openThread("Root"); + assertUnreadLessThan(room2, 4); + openThread("Root2"); + assertRead(room2); + closeThreadsPanel(); + goTo(room1); + assertRead(room2); + + // When the latest message in a thread is redacted + receiveMessages(room2, [redactionOf("ThreadMsg2")]); + + // Then the room and thread are still read + assertStillRead(room2); + goTo(room2); + assertReadThread("Root"); + }); + + // XXX: fails because the unread count is still 1 when it should be 0 (this is a genuine stuck unread case) + it.skip("Reading an unread thread after a redaction of the latest message makes it read", () => { + // Given an unread thread where the latest message was redacted + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]); + assertUnread(room2, 3); + receiveMessages(room2, [redactionOf("ThreadMsg2")]); + assertUnread(room2, 2); + goTo(room2); + assertUnreadThread("Root"); + + // When I read the thread + openThread("Root"); + assertRead(room2); + closeThreadsPanel(); + goTo(room1); - it.skip("Reading an unread thread after a redaction of the latest message makes it read", () => {}); - it.skip("Reading an unread thread after a redaction of an older message makes it read", () => {}); - it.skip("Marking an unread thread as read after a redaction makes it read", () => {}); - it.skip("Sending and redacting a message after marking the thread as read makes it unread", () => {}); - it.skip("?? Redacting a message after marking the thread as read makes it unread", () => {}); - it.skip("Reacting to a redacted message leaves the thread read", () => {}); - it.skip("Editing a redacted message leaves the thread read", () => {}); + // Then the thread is read + assertRead(room2); + goTo(room2); + assertReadThread("Root"); + }); + // XXX: fails because the unread count is still 1 when it should be 0 + it.skip("Reading an unread thread after a redaction of the latest message makes it read after restart", () => { + // Given a redacted message is not counted in the unread count + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]); + assertUnread(room2, 3); + receiveMessages(room2, [redactionOf("ThreadMsg2")]); + assertUnread(room2, 2); + goTo(room2); + assertUnreadThread("Root"); + openThread("Root"); + assertRead(room2); + closeThreadsPanel(); + goTo(room1); + assertRead(room2); + goTo(room2); + assertReadThread("Root"); + + // When I restart + saveAndReload(); + + // Then the room is still read + assertRead(room2); + }); + // XXX: fails because the unread count is still 1 when it should be 0 + it.skip("Reading an unread thread after a redaction of an older message makes it read", () => { + // Given an unread thread where an older message was redacted + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]); + assertUnread(room2, 3); + receiveMessages(room2, [redactionOf("ThreadMsg1")]); + assertUnread(room2, 2); + goTo(room2); + assertUnreadThread("Root"); + + // When I read the thread + openThread("Root"); + assertRead(room2); + closeThreadsPanel(); + goTo(room1); + + // Then the thread is read + assertRead(room2); + goTo(room2); + assertReadThread("Root"); + }); + // XXX: fails because the room has an unread dot after I marked it as read + it.skip("Marking an unread thread as read after a redaction makes it read", () => { + // Given an unread thread where an older message was redacted + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]); + assertUnread(room2, 3); + receiveMessages(room2, [redactionOf("ThreadMsg1")]); + assertUnread(room2, 2); + + // When I mark the room as read + markAsRead(room2); + assertRead(room2); + + // Then the thread is read + assertRead(room2); + goTo(room2); + assertReadThread("Root"); + }); + // XXX: fails because the room has an unread dot after I marked it as read + it.skip("Sending and redacting a message after marking the thread as read leaves it read", () => { + // Given a thread exists and is marked as read + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]); + assertUnread(room2, 3); + markAsRead(room2); + assertRead(room2); + + // When I send and redact a message + receiveMessages(room2, [threadedOff("Root", "Msg3")]); + assertUnread(room2, 1); + receiveMessages(room2, [redactionOf("Msg3")]); + + // Then the room and thread are read + assertRead(room2); + goTo(room2); + assertReadThread("Root"); + }); + // XXX: fails because the room has an unread dot after I marked it as read + it.skip("Redacting a message after marking the thread as read leaves it read", () => { + // Given a thread exists and is marked as read + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]); + assertUnread(room2, 3); + markAsRead(room2); + assertRead(room2); + + // When I redact a message + receiveMessages(room2, [redactionOf("ThreadMsg1")]); + + // Then the room and thread are read + assertRead(room2); + goTo(room2); + assertReadThread("Root"); + }); + // TODO: Doesn't work because the test setup can't (yet) find the ID of a redacted message + it.skip("Reacting to a redacted message leaves the thread read", () => { + // Given a message in a thread was redacted and everything is read + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 2); + goTo(room2); + assertUnread(room2, 1); + openThread("Root"); + assertRead(room2); + goTo(room1); + + // When we receive a reaction to the redacted event + // TODO: doesn't work yet because we need to be able to look up + // the ID of Msg2 even though it has now disappeared from the + // timeline. + receiveMessages(room2, [reactionTo(room2, "Msg2")]); + + // Then the room is unread + assertStillRead(room2); + }); + // TODO: Doesn't work because the test setup can't (yet) find the ID of a redacted message + it.skip("Editing a redacted message leaves the thread read", () => { + // Given a message in a thread was redacted and everything is read + goTo(room1); + receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 2); + goTo(room2); + assertUnread(room2, 1); + openThread("Root"); + assertRead(room2); + goTo(room1); + + // When we receive an edit of the redacted message + // TODO: doesn't work yet because we need to be able to look up + // the ID of Msg2 even though it has now disappeared from the + // timeline. + receiveMessages(room2, [editOf("Msg2", "New Msg2")]); + + // Then the room is unread + assertStillRead(room2); + }); it.skip("?? Reading a reaction to a redacted message marks the thread as read", () => {}); it.skip("?? Reading an edit of a redacted message marks the thread as read", () => {}); it.skip("Reading a reply to a redacted message marks the thread as read", () => {}); + it.skip("Reading a thread root when its only message has been redacted leaves the room read", () => {}); it.skip("A thread with an unread redaction is still unread after restart", () => {}); - it.skip("A thread with a read redaction is still read after restart", () => {}); + it("A thread with a read redaction is still read after restart", () => { + // Given my receipt points at a redacted thread message + goTo(room1); + receiveMessages(room2, [ + "Root", + threadedOff("Root", "ThreadMsg1"), + threadedOff("Root", "ThreadMsg2"), + "Root2", + threadedOff("Root2", "Root2->A"), + ]); + assertUnread(room2, 5); + goTo(room2); + assertUnreadThread("Root"); + openThread("Root"); + assertUnreadLessThan(room2, 4); + openThread("Root2"); + assertRead(room2); + closeThreadsPanel(); + goTo(room1); + assertRead(room2); + receiveMessages(room2, [redactionOf("ThreadMsg2")]); + assertStillRead(room2); + goTo(room2); + assertReadThread("Root"); + + // When I restart + saveAndReload(); + + // Then the room is still read + assertRead(room2); + }); it.skip("A thread with an unread reply to a redacted message is still unread after restart", () => {}); it.skip("A thread with a read replt to a redacted message is still read after restart", () => {}); });