From 398ade2cb9806c8f95b3b984cd4349debbb8d81f Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 19 Jul 2023 14:02:13 +0100 Subject: [PATCH 01/37] Add some fantasy tests --- .../e2e/read-receipts/read-receipts.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 25334f2a22d..c9c7525b37f 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -110,6 +110,76 @@ describe("Read receipts", () => { }); }); + { + /* + tst("Editing a message makes a room unread", () => { + // Given I am not in the room + goTo("room1"); + + // When an edit appears in the room + sendMessages("room2", ["Msg1", editOf("Msg1")]); + + // Then it becomes unread + assertUnread("room2"); + }); + */ + /* + tst("Reading an edit makes the room read", () => { + // Given an edit made a room unread + goTo("room1"); + sendMessages("room2", ["Msg1", editOf("Msg1")]); + assertUnread("room2"); // (Sanity) + + // When I read it + goTo("room2"); + + // Then the room becomes unread and stays unread + assertRead("room2"); + goTo("room1"); + assertRead("room2"); + }); + */ + /* + tst("Reading the main timeline does not mark a thread message as read", () => { + // Given a thread exists + goTo("room1"); + sendMessages("room2", ["Msg1", threadedOff("Msg1"), threadedOff("Msg1")]); + assertUnread("room2"); // (Sanity) + + // When I read the main timeline + goTo("room2"); + + // Then the room briefly appears read (!) + assertRead("room2"); + + // But when I switch away, it is unread again because we didn't read the thread + goTo("room1"); + assertUnread("room2"); + }); + */ + /* + tst("Reading an edit of a thread root makes the room read", () => { + // Given a fully-read thread exists + goTo("room2"); + sendMessages("room2", ["Msg1", threadedOff("Msg1")]); + openThread("Msg1"); + goTo("room1"); + assertRead("room2"); + + // When the thread root is edited + sendMessages("room2", [editOf("Msg1")]); + + // And I read that edit + goTo("room2"); + + // Then the room becomes unread and stays unread + assertRead("room2"); + goTo("room1"); + assertRead("room2"); + }); + */ + } + afterEach(() => { cy.stopHomeserver(homeserver); }); From 031a6a35d1e1aa0363f7f32a43135ee3537d456c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 19 Jul 2023 15:50:59 +0100 Subject: [PATCH 02/37] Turn commented code into pretend-real code --- .../e2e/read-receipts/read-receipts.spec.ts | 174 +++++++++++------- 1 file changed, 104 insertions(+), 70 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index c9c7525b37f..24d5bbed350 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -110,76 +110,6 @@ describe("Read receipts", () => { }); }); - { - /* - tst("Editing a message makes a room unread", () => { - // Given I am not in the room - goTo("room1"); - - // When an edit appears in the room - sendMessages("room2", ["Msg1", editOf("Msg1")]); - - // Then it becomes unread - assertUnread("room2"); - }); - */ - /* - tst("Reading an edit makes the room read", () => { - // Given an edit made a room unread - goTo("room1"); - sendMessages("room2", ["Msg1", editOf("Msg1")]); - assertUnread("room2"); // (Sanity) - - // When I read it - goTo("room2"); - - // Then the room becomes unread and stays unread - assertRead("room2"); - goTo("room1"); - assertRead("room2"); - }); - */ - /* - tst("Reading the main timeline does not mark a thread message as read", () => { - // Given a thread exists - goTo("room1"); - sendMessages("room2", ["Msg1", threadedOff("Msg1"), threadedOff("Msg1")]); - assertUnread("room2"); // (Sanity) - - // When I read the main timeline - goTo("room2"); - - // Then the room briefly appears read (!) - assertRead("room2"); - - // But when I switch away, it is unread again because we didn't read the thread - goTo("room1"); - assertUnread("room2"); - }); - */ - /* - tst("Reading an edit of a thread root makes the room read", () => { - // Given a fully-read thread exists - goTo("room2"); - sendMessages("room2", ["Msg1", threadedOff("Msg1")]); - openThread("Msg1"); - goTo("room1"); - assertRead("room2"); - - // When the thread root is edited - sendMessages("room2", [editOf("Msg1")]); - - // And I read that edit - goTo("room2"); - - // Then the room becomes unread and stays unread - assertRead("room2"); - goTo("room1"); - assertRead("room2"); - }); - */ - } - afterEach(() => { cy.stopHomeserver(homeserver); }); @@ -424,4 +354,108 @@ describe("Read receipts", () => { }); }); }); + + class MessageSpec {} + + type Message = string | MessageSpec; + + function goTo(room: string) { + throw new Error("todo"); + } + + function openThread(rootMessage: string) { + throw new Error("todo"); + } + + function sendMessages(room: string, messages: Message[]) { + throw new Error("todo"); + } + + function editOf(originalMessage: string): MessageSpec { + throw new Error("todo"); + } + + function threadedOff(rootMessage: string): MessageSpec { + throw new Error("todo"); + } + + function assertRead(room: string) { + throw new Error("todo"); + } + + function assertUnread(room: string) { + throw new Error("todo"); + } + + describe("editing messages", () => { + describe("in the main timeline", () => { + test("Editing a message makes a room unread", () => { + // Given I am not in the room + goTo("room1"); + + // When an edit appears in the room + sendMessages("room2", ["Msg1", editOf("Msg1")]); + + // Then it becomes unread + assertUnread("room2"); + }); + test("Reading an edit makes the room read", () => { + // Given an edit made a room unread + goTo("room1"); + sendMessages("room2", ["Msg1", editOf("Msg1")]); + assertUnread("room2"); // (Sanity) + + // When I read it + goTo("room2"); + + // Then the room becomes unread and stays unread + assertRead("room2"); + goTo("room1"); + assertRead("room2"); + }); + }); + describe("in threads", () => { + test("Reading an edit of a thread root makes the room read", () => { + // Given a fully-read thread exists + goTo("room2"); + sendMessages("room2", ["Msg1", threadedOff("Msg1")]); + openThread("Msg1"); + goTo("room1"); + assertRead("room2"); + + // When the thread root is edited + sendMessages("room2", [editOf("Msg1")]); + + // And I read that edit + goTo("room2"); + + // Then the room becomes unread and stays unread + assertRead("room2"); + goTo("room1"); + assertRead("room2"); + }); + }); + }); + + describe("threads", () => { + // Thread-specific variants live inside other sections, but when thread + // tests don't live anywhere else, they live here. + + test("Reading the main timeline does not mark a thread message as read", () => { + // Given a thread exists + goTo("room1"); + sendMessages("room2", ["Msg1", threadedOff("Msg1"), threadedOff("Msg1")]); + assertUnread("room2"); // (Sanity) + + // When I read the main timeline + goTo("room2"); + + // Then the room briefly appears read (!) + assertRead("room2"); + + // But when I switch away, it is unread again because we didn't read the thread + goTo("room1"); + assertUnread("room2"); + }); + }); }); From 4445bbd6bf84dec7529255b422377995c68190c9 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 20 Jul 2023 17:52:19 +0100 Subject: [PATCH 03/37] First pass at a list of all the cases we should test --- .../e2e/read-receipts/read-receipts.spec.ts | 166 ++++++++++++++++-- 1 file changed, 151 insertions(+), 15 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 24d5bbed350..1923c3caa52 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -387,6 +387,52 @@ describe("Read receipts", () => { throw new Error("todo"); } + describe("new messages", () => { + describe("in the main timeline", () => { + test("Sending a message makes a room unread", () => {}); + test("Reading latest message makes the room read", () => {}); + test("Reading an older message leaves the room unread", () => {}); + test("Marking a room as read makes it read", () => {}); + test("Sending a new message after marking as read makes it unread", () => {}); + test("A room with a new message is still unread after restart", () => {}); + test("A room where all messages are read is still read after restart", () => {}); + }); + + describe("in threads", () => { + test("Sending a message makes a room unread", () => {}); + test("Reading the last threaded message makes the room read", () => {}); + test("Reading a thread message makes the thread read", () => {}); + test("Reading an older thread message (via permalink) leaves the thread unread", () => {}); + test("Reading only one thread's message does not make the room read", () => {}); + test("Reading only one thread's message make that thread read but not others", () => {}); + test("Reading the main timeline does not mark a thread message as read", () => { + // Given a thread exists + goTo("room1"); + sendMessages("room2", ["Msg1", threadedOff("Msg1"), threadedOff("Msg1")]); + assertUnread("room2"); // (Sanity) + + // When I read the main timeline + goTo("room2"); + + // Then the room briefly appears read (!) + assertRead("room2"); + + // But when I switch away, it is unread again because we didn't read the thread + goTo("room1"); + assertUnread("room2"); + }); + test("Marking a room with unread threads as read makes it read", () => {}); + test("Sending a new thread message after marking as read makes it unread", () => {}); + test("A room with a new threaded message is still unread after restart", () => {}); + test("A room where all threaded messages are read is still read after restart", () => {}); + }); + + describe("thread roots", () => { + test("Reading a thread root does not mark the thread as read", () => {}); + test("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); + }); + }); + describe("editing messages", () => { describe("in the main timeline", () => { test("Editing a message makes a room unread", () => { @@ -413,8 +459,23 @@ describe("Read receipts", () => { goTo("room1"); assertRead("room2"); }); + test("Marking a room as read after an edit makes it read", () => {}); + test("Editing a message after marking as read makes the room unread", () => {}); + test("A room with an edit is still unread after restart", () => {}); + test("A room where all edits are read is still read after restart", () => {}); }); + describe("in threads", () => { + test("An edit of a threaded message makes the room unread", () => {}); + test("Reading an edit of a threaded message makes the room read", () => {}); + test("Marking a room as read after an edit in a thread makes it read", () => {}); + test("Editing a thread message after marking as read makes the room unread", () => {}); + test("A room with an edited threaded message is still unread after restart", () => {}); + test("A room where all threaded edits are read is still read after restart", () => {}); + }); + + describe("thread roots", () => { + test("An edit of a thread root makes the room unread", () => {}); test("Reading an edit of a thread root makes the room read", () => { // Given a fully-read thread exists goTo("room2"); @@ -434,28 +495,103 @@ describe("Read receipts", () => { goTo("room1"); assertRead("room2"); }); + test("Marking a room as read after an edit of a thread root makes it read", () => {}); + test("Editing a thread root after marking as read makes the room unread", () => {}); + }); + }); + + 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", () => { + test("Reacting to a message makes a room unread", () => {}); + test("Reading a reaction makes the room read", () => {}); + test("Marking a room as read after a reaction makes it read", () => {}); + test("Reacting to a message after marking as read makes the room unread", () => {}); + test("A room with a reaction is still unread after restart", () => {}); + test("A room where all reactions are read is still read after restart", () => {}); + }); + + describe("in threads", () => { + test("A reaction to a threaded message makes the room unread", () => {}); + test("Reading a reaction to a threaded message makes the room read", () => {}); + test("Marking a room as read after a reaction in a thread makes it read", () => {}); + test("Reacting to a thread message after marking as read makes the room unread", () => {}); + test("A room with a reaction to a threaded message is still unread after restart", () => {}); + test("A room where all reactions in threads are read is still read after restart", () => {}); + }); + + describe("thread roots", () => { + test("A reaction to a thread root makes the room unread", () => {}); + test("Reading a reaction to a thread root makes the room read", () => {}); + test("Marking a room as read after a reaction to a thread root makes it read", () => {}); + test("Reacting to a thread root after marking as read makes the room unread", () => {}); }); }); - describe("threads", () => { - // Thread-specific variants live inside other sections, but when thread - // tests don't live anywhere else, they live here. + describe("orphan messages", () => { + test("A message in an unknown thread is not visible and the room is read", () => {}); + test("When a message's thread root appears later the thread appears and the room is unread", () => {}); + test("An edit of an unknown message is not visible and the room is read", () => {}); + test("When an edit's message appears later the edited version appears and the room is unread", () => {}); + test("A reaction to an unknown message is not visible and the room is read", () => {}); + test("When an reactions's message appears later it appears and the room is unread", () => {}); + // Harder: validate that we request the messages we are missing? + }); - test("Reading the main timeline does not mark a thread message as read", () => { - // Given a thread exists - goTo("room1"); - sendMessages("room2", ["Msg1", threadedOff("Msg1"), threadedOff("Msg1")]); - assertUnread("room2"); // (Sanity) + describe("orphan receipts", () => { + // Later: when we have order in receipts, we can change these tests to + // make receipts still work, even when their message is not found. + test("A receipt for an unknown message does not change the state of an unread room", () => {}); + test("A receipt for an unknown message does not change the state of a read room", () => {}); + test("A threaded receipt for an unknown message does not change the state of an unread thread", () => {}); + test("A threaded receipt for an unknown message does not change the state of a read thread", () => {}); + test("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {}); + test("A threaded receipt for an unknown thread does not change the state of a read thread", () => {}); + test("A threaded receipt for a message on main does not change the state of an unread room", () => {}); + test("A threaded receipt for a message on main does not change the state of a read room", () => {}); + test("A main receipt for a message on a thread does not change the state of an unread room", () => {}); + test("A main receipt for a message on a thread does not change the state of a read room", () => {}); + test("A threaded receipt for a thread root does not mark it as read", () => {}); + // Harder: validate that we request the messages we are missing? + }); - // When I read the main timeline - goTo("room2"); + describe("Message ordering", () => { + describe("in the main timeline", () => { + test("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {}); + test("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {}); + }); - // Then the room briefly appears read (!) - assertRead("room2"); + describe("in threads", () => { + // These don't pass yet - we need MSC4033 - we don't even know the Sync order yet + test.skip("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {}); + test.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {}); + + // These pass now and should not later - we should use order from MSC4033 instead of ts + // These are broken out + test("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {}); + test("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {}); + test("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {}); + test("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {}); + test("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {}); + test("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {}); + }); - // But when I switch away, it is unread again because we didn't read the thread - goTo("room1"); - assertUnread("room2"); + describe("thread roots", () => { + test("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {}); + test("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); + test("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {}); + test("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); }); }); + + describe("Ignored events", () => { + test("If all events after receipt are unimportant, the room is read", () => {}); + test("Sending an important event after unimportant ones makes the room unread", () => {}); + test("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); + }); }); From e9cc49a5d5ca918124faae5e1db3f1ba2b6d262f Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 21 Jul 2023 08:22:48 +0100 Subject: [PATCH 04/37] List test cases related to redactions --- .../e2e/read-receipts/read-receipts.spec.ts | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 1923c3caa52..fe64161eb59 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -533,7 +533,66 @@ describe("Read receipts", () => { }); }); - describe("orphan messages", () => { + describe("redactions", () => { + describe("in the main timeline", () => { + // One of the following two must be right: + test("Redacting the message pointed to by my receipt leaves the room read", () => {}); + test("Redacting a message after it was read makes the room unread", () => {}); + + test("Reading an unread room after a redaction of the latest message makes it read", () => {}); + test("Reading an unread room after a redaction of an older message makes it read", () => {}); + test("Marking an unread room as read after a redaction makes it read", () => {}); + test("Sending and redacting a message after marking the room as read makes it unread", () => {}); + test("?? Redacting a message after marking the room as read makes it unread", () => {}); + test("Reacting to a redacted message leaves the room read", () => {}); + test("Editing a redacted message leaves the room read", () => {}); + + test("?? Reading a reaction to a redacted message marks the room as read", () => {}); + test("?? Reading an edit of a redacted message marks the room as read", () => {}); + test("Reading a reply to a redacted message marks the room as read", () => {}); + + test("A room with an unread redaction is still unread after restart", () => {}); + test("A room with a read redaction is still read after restart", () => {}); + }); + + describe("in threads", () => { + // One of the following two must be right: + test("Redacting the threaded message pointed to by my receipt leaves the room read", () => {}); + test("Redacting a threaded message after it was read makes the room unread", () => {}); + + test("Reading an unread thread after a redaction of the latest message makes it read", () => {}); + test("Reading an unread thread after a redaction of an older message makes it read", () => {}); + test("Marking an unread thread as read after a redaction makes it read", () => {}); + test("Sending and redacting a message after marking the thread as read makes it unread", () => {}); + test("?? Redacting a message after marking the thread as read makes it unread", () => {}); + test("Reacting to a redacted message leaves the thread read", () => {}); + test("Editing a redacted message leaves the thread read", () => {}); + + test("?? Reading a reaction to a redacted message marks the thread as read", () => {}); + test("?? Reading an edit of a redacted message marks the thread as read", () => {}); + test("Reading a reply to a redacted message marks the thread as read", () => {}); + + test("A thread with an unread redaction is still unread after restart", () => {}); + test("A thread with a read redaction is still read after restart", () => {}); + test("A thread with an unread reply to a redacted message is still unread after restart", () => {}); + test("A thread with a read replt to a redacted message is still read after restart", () => {}); + }); + + describe("thread roots", () => { + // One of the following two must be right: + test("Redacting a thread root after it was read leaves the room read", () => {}); + test("Redacting a thread root after it was read makes the room unread", () => {}); + + test("Redacting the root of an unread thread makes the room read", () => {}); + test("Sending a threaded message onto a redacted thread root leaves the room read", () => {}); + test("Reacting to a redacted thread root leaves the room read", () => {}); + test("Editing a redacted thread root leaves the room read", () => {}); + test("Replying to a redacted thread root makes the room unread", () => {}); + test("Reading a reply to a redacted thread root makes the room read", () => {}); + }); + }); + + describe("messages with missing referents", () => { test("A message in an unknown thread is not visible and the room is read", () => {}); test("When a message's thread root appears later the thread appears and the room is unread", () => {}); test("An edit of an unknown message is not visible and the room is read", () => {}); @@ -543,7 +602,7 @@ describe("Read receipts", () => { // Harder: validate that we request the messages we are missing? }); - describe("orphan receipts", () => { + describe("receipts with missing events", () => { // Later: when we have order in receipts, we can change these tests to // make receipts still work, even when their message is not found. test("A receipt for an unknown message does not change the state of an unread room", () => {}); From 20ab4addecf946695880a39cc34026439b5b2248 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 21 Jul 2023 08:26:42 +0100 Subject: [PATCH 05/37] Add testcases about paging up --- cypress/e2e/read-receipts/read-receipts.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index fe64161eb59..3ce7cde7b38 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -653,4 +653,12 @@ describe("Read receipts", () => { test("Sending an important event after unimportant ones makes the room unread", () => {}); test("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); }); + + describe("Paging up", () => { + test("Paging up through old messages after a room is read leaves the room read", () => {}); + test("Paging up through old messages of an unread room leaves the room unread", () => {}); + test("Paging up to find old threads that were previously read leaves the room read", () => {}); + test("?? Paging up to find old threads that were never read marks the room unread", () => {}); + test("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); + }); }); From 80481896c8bf8fa74210dc5ca0a59df14e5e0b7c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 21 Jul 2023 08:27:43 +0100 Subject: [PATCH 06/37] Add a case about notification counts --- cypress/e2e/read-receipts/read-receipts.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 3ce7cde7b38..cf218a0a364 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -660,5 +660,6 @@ describe("Read receipts", () => { test("Paging up to find old threads that were previously read leaves the room read", () => {}); test("?? Paging up to find old threads that were never read marks the room unread", () => {}); test("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); + test("Notification count remains steady when reading threads that contain seen notificiations", () => {}); }); }); From e6b5b125d1104fe518314302efb7749897d1a584 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 21 Jul 2023 08:47:09 +0100 Subject: [PATCH 07/37] More test cases related to replies, notifications, room list --- .../e2e/read-receipts/read-receipts.spec.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index cf218a0a364..6b302bdeaa0 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -430,6 +430,8 @@ describe("Read receipts", () => { describe("thread roots", () => { test("Reading a thread root does not mark the thread as read", () => {}); test("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); + test("Creating a new thread based on a reply makes the room unread", () => {}); + test("Reading a thread whose root is a reply makes the room read", () => {}); }); }); @@ -461,6 +463,8 @@ describe("Read receipts", () => { }); test("Marking a room as read after an edit makes it read", () => {}); test("Editing a message after marking as read makes the room unread", () => {}); + test("Editing a reply after reading it makes the room unread", () => {}); + test("Editing a reply after marking as read makes the room unread", () => {}); test("A room with an edit is still unread after restart", () => {}); test("A room where all edits are read is still read after restart", () => {}); }); @@ -497,6 +501,8 @@ describe("Read receipts", () => { }); test("Marking a room as read after an edit of a thread root makes it read", () => {}); test("Editing a thread root after marking as read makes the room unread", () => {}); + test("Marking a room as read after an edit of a thread root that is a reply makes it read", () => {}); + test("Editing a thread root that is a reply after marking as read makes the room unread but not the thread", () => {}); }); }); @@ -529,7 +535,7 @@ describe("Read receipts", () => { test("A reaction to a thread root makes the room unread", () => {}); test("Reading a reaction to a thread root makes the room read", () => {}); test("Marking a room as read after a reaction to a thread root makes it read", () => {}); - test("Reacting to a thread root after marking as read makes the room unread", () => {}); + test("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {}); }); }); @@ -660,6 +666,29 @@ describe("Read receipts", () => { test("Paging up to find old threads that were previously read leaves the room read", () => {}); test("?? Paging up to find old threads that were never read marks the room unread", () => {}); test("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); - test("Notification count remains steady when reading threads that contain seen notificiations", () => {}); + }); + + describe("Room list order", () => { + test("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {}); + }); + + describe("Notifications", () => { + describe("in the main timeline", () => { + test("A new message that mentions me shows a notification", () => {}); + test("Reading a notifying message reduces the notification count in the room list, space and tab", () => {}); + test("Reading the last notifying message removes the notification marker from room list, space and tab", () => {}); + test("Editing a message to mentions me shows a notification", () => {}); + test("Reading the last notifying edited message removes the notification marker", () => {}); + test("Redacting a notifying message removes the notification marker", () => {}); + }); + + describe("in threads", () => { + test("A new threaded message that mentions me shows a notification", () => {}); + test("Reading a notifying threaded message removes the notification count", () => {}); + test("Notification count remains steady when reading threads that contain seen notifications", () => {}); + test("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {}); + test("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {}); + test("Redacting a notifying threaded message removes the notification marker", () => {}); + }); }); }); From 8b71465af5f39767327c089c244f8ac57f1744fa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 31 Jul 2023 17:46:34 +0100 Subject: [PATCH 08/37] Iterate tests --- .../e2e/read-receipts/read-receipts.spec.ts | 460 ++++++++++-------- cypress/support/views.ts | 5 +- 2 files changed, 271 insertions(+), 194 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 6b302bdeaa0..e9c42385bb9 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -19,7 +19,9 @@ limitations under the License. import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; +import type { Room } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import Chainable = Cypress.Chainable; describe("Read receipts", () => { const userName = "Mae"; @@ -355,154 +357,226 @@ describe("Read receipts", () => { }); }); - class MessageSpec {} + abstract class MessageSpec { + public abstract getContent(room: Room): Promise>; + } type Message = string | MessageSpec; function goTo(room: string) { - throw new Error("todo"); + cy.viewRoomByName(room); + } + + function findRoomByName(room: string): Chainable { + return cy.getClient().then((cli) => { + return cli.getRooms().find((r) => r.name === room); + }); } function openThread(rootMessage: string) { - throw new Error("todo"); + cy.get(".mx_RoomView_body").within(() => { + cy.contains(".mx_EventTile[data-scroll-tokens]", rootMessage) + .realHover() + .findByRole("button", { name: "Reply in thread" }) + .click(); + }); + cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); } + // Sends messages into given room as a bot function sendMessages(room: string, messages: Message[]) { - throw new Error("todo"); + findRoomByName(room).then(async ({ roomId }) => { + const room = bot.getRoom(roomId); + for (const message of messages) { + if (typeof message === "string") { + await bot.sendTextMessage(roomId, message); + } else { + await bot.sendMessage(roomId, await message.getContent(room)); + } + } + }); } - function editOf(originalMessage: string): MessageSpec { - throw new Error("todo"); + async function getMessage(room: Room, message: string): Promise { + const ev = room.timeline.find((e) => e.getContent().body === message); + if (ev) return ev; + + return new Promise((resolve) => { + room.on("Room.timeline" as any, (ev: MatrixEvent) => { + if (ev.getContent().body === message) { + resolve(ev); + } + }); + }); + } + + function editOf(originalMessage: string, newMessage: string): MessageSpec { + return new (class extends MessageSpec { + public async getContent(room: Room): Promise> { + const ev = await getMessage(room, originalMessage); + + const content = ev.getContent(); + return { + "msgtype": content.msgtype, + "body": `* ${newMessage}`, + "m.new_content": { + msgtype: content.msgtype, + body: newMessage, + }, + }; + } + })(); } - function threadedOff(rootMessage: string): MessageSpec { - throw new Error("todo"); + function threadedOff(rootMessage: string, newMessage: string): MessageSpec { + return new (class extends MessageSpec { + public async getContent(room: Room): Promise> { + const ev = await getMessage(room, rootMessage); + + return { + "msgtype": "m.text", + "body": newMessage, + "m.relates_to": { + event_id: ev.getId(), + is_falling_back: true, + rel_type: "m.thread", + }, + }; + } + })(); } function assertRead(room: string) { - throw new Error("todo"); + return cy.findByRole("treeitem", { name: new RegExp("^" + room) }).within(() => { + cy.get(".mx_NotificationBadge_count").should("not.exist"); + }); } function assertUnread(room: string) { - throw new Error("todo"); + return cy.findByRole("treeitem", { name: new RegExp("^" + room) }).within(() => { + cy.get(".mx_NotificationBadge_count").should("exist"); + }); } + const room1 = selectedRoomName; + const room2 = otherRoomName; + describe("new messages", () => { describe("in the main timeline", () => { - test("Sending a message makes a room unread", () => {}); - test("Reading latest message makes the room read", () => {}); - test("Reading an older message leaves the room unread", () => {}); - test("Marking a room as read makes it read", () => {}); - test("Sending a new message after marking as read makes it unread", () => {}); - test("A room with a new message is still unread after restart", () => {}); - test("A room where all messages are read is still read after restart", () => {}); + it.skip("Sending a message makes a room unread", () => {}); + it.skip("Reading latest message makes the room read", () => {}); + it.skip("Reading an older message leaves the room unread", () => {}); + it.skip("Marking a room as read makes it read", () => {}); + it.skip("Sending a new message after marking as read makes it unread", () => {}); + it.skip("A room with a new message is still unread after restart", () => {}); + it.skip("A room where all messages are read is still read after restart", () => {}); }); describe("in threads", () => { - test("Sending a message makes a room unread", () => {}); - test("Reading the last threaded message makes the room read", () => {}); - test("Reading a thread message makes the thread read", () => {}); - test("Reading an older thread message (via permalink) leaves the thread unread", () => {}); - test("Reading only one thread's message does not make the room read", () => {}); - test("Reading only one thread's message make that thread read but not others", () => {}); - test("Reading the main timeline does not mark a thread message as read", () => { + it.skip("Sending a message makes a room unread", () => {}); + it.skip("Reading the last threaded message makes the room read", () => {}); + it.skip("Reading a thread message makes the thread read", () => {}); + it.skip("Reading an older thread message (via permalink) leaves the thread unread", () => {}); + it.skip("Reading only one thread's message does not make the room read", () => {}); + it.skip("Reading only one thread's message make that thread read but not others", () => {}); + it("Reading the main timeline does not mark a thread message as read", () => { // Given a thread exists - goTo("room1"); - sendMessages("room2", ["Msg1", threadedOff("Msg1"), threadedOff("Msg1")]); - assertUnread("room2"); // (Sanity) + goTo(room1); + sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2); // (Sanity) // When I read the main timeline - goTo("room2"); + goTo(room2); - // Then the room briefly appears read (!) - assertRead("room2"); + // Then room does not appear unread + assertUnread(room2); - // But when I switch away, it is unread again because we didn't read the thread - goTo("room1"); - assertUnread("room2"); + // Until we open the thread + openThread("Msg1"); + assertRead(room2); }); - test("Marking a room with unread threads as read makes it read", () => {}); - test("Sending a new thread message after marking as read makes it unread", () => {}); - test("A room with a new threaded message is still unread after restart", () => {}); - test("A room where all threaded messages are read is still read after restart", () => {}); + it.skip("Marking a room with unread threads as read makes it read", () => {}); + it.skip("Sending a new thread message after marking as read makes it unread", () => {}); + it.skip("A room with a new threaded message is still unread after restart", () => {}); + it.skip("A room where all threaded messages are read is still read after restart", () => {}); }); describe("thread roots", () => { - test("Reading a thread root does not mark the thread as read", () => {}); - test("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); - test("Creating a new thread based on a reply makes the room unread", () => {}); - test("Reading a thread whose root is a reply makes the room read", () => {}); + it.skip("Reading a thread root does not mark the thread as read", () => {}); + it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); + it.skip("Creating a new thread based on a reply makes the room unread", () => {}); + it.skip("Reading a thread whose root is a reply makes the room read", () => {}); }); }); describe("editing messages", () => { describe("in the main timeline", () => { - test("Editing a message makes a room unread", () => { + it.skip("Editing a message makes a room unread", () => { // Given I am not in the room - goTo("room1"); + goTo(room1); // When an edit appears in the room - sendMessages("room2", ["Msg1", editOf("Msg1")]); + sendMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); // Then it becomes unread - assertUnread("room2"); + assertUnread(room2); }); - test("Reading an edit makes the room read", () => { + it.skip("Reading an edit makes the room read", () => { // Given an edit made a room unread - goTo("room1"); - sendMessages("room2", ["Msg1", editOf("Msg1")]); - assertUnread("room2"); // (Sanity) + goTo(room1); + sendMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); + assertUnread(room2); // (Sanity) // When I read it - goTo("room2"); + goTo(room2); // Then the room becomes unread and stays unread - assertRead("room2"); - goTo("room1"); - assertRead("room2"); + assertRead(room2); + goTo(room1); + assertRead(room2); }); - test("Marking a room as read after an edit makes it read", () => {}); - test("Editing a message after marking as read makes the room unread", () => {}); - test("Editing a reply after reading it makes the room unread", () => {}); - test("Editing a reply after marking as read makes the room unread", () => {}); - test("A room with an edit is still unread after restart", () => {}); - test("A room where all edits are read is still read after restart", () => {}); + it.skip("Marking a room as read after an edit makes it read", () => {}); + it.skip("Editing a message after marking as read makes the room unread", () => {}); + it.skip("Editing a reply after reading it makes the room unread", () => {}); + it.skip("Editing a reply after marking as read makes the room unread", () => {}); + it.skip("A room with an edit is still unread after restart", () => {}); + it.skip("A room where all edits are read is still read after restart", () => {}); }); describe("in threads", () => { - test("An edit of a threaded message makes the room unread", () => {}); - test("Reading an edit of a threaded message makes the room read", () => {}); - test("Marking a room as read after an edit in a thread makes it read", () => {}); - test("Editing a thread message after marking as read makes the room unread", () => {}); - test("A room with an edited threaded message is still unread after restart", () => {}); - test("A room where all threaded edits are read is still read after restart", () => {}); + it.skip("An edit of a threaded message makes the room unread", () => {}); + it.skip("Reading an edit of a threaded message makes the room read", () => {}); + it.skip("Marking a room as read after an edit in a thread makes it read", () => {}); + it.skip("Editing a thread message after marking as read makes the room unread", () => {}); + it.skip("A room with an edited threaded message is still unread after restart", () => {}); + it.skip("A room where all threaded edits are read is still read after restart", () => {}); }); describe("thread roots", () => { - test("An edit of a thread root makes the room unread", () => {}); - test("Reading an edit of a thread root makes the room read", () => { + it.skip("An edit of a thread root makes the room unread", () => {}); + it("Reading an edit of a thread root makes the room read", () => { // Given a fully-read thread exists - goTo("room2"); - sendMessages("room2", ["Msg1", threadedOff("Msg1")]); + goTo(room2); + sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); openThread("Msg1"); - goTo("room1"); - assertRead("room2"); + goTo(room1); + assertRead(room2); // When the thread root is edited - sendMessages("room2", [editOf("Msg1")]); + sendMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); // And I read that edit - goTo("room2"); + goTo(room2); // Then the room becomes unread and stays unread - assertRead("room2"); - goTo("room1"); - assertRead("room2"); + assertRead(room2); + goTo(room1); + assertRead(room2); }); - test("Marking a room as read after an edit of a thread root makes it read", () => {}); - test("Editing a thread root after marking as read makes the room unread", () => {}); - test("Marking a room as read after an edit of a thread root that is a reply makes it read", () => {}); - test("Editing a thread root that is a reply after marking as read makes the room unread but not the thread", () => {}); + it.skip("Marking a room as read after an edit of a thread root makes it read", () => {}); + it.skip("Editing a thread root after marking as read makes the room unread", () => {}); + it.skip("Marking a room as read after an edit of a thread root that is a reply makes it read", () => {}); + it.skip("Editing a thread root that is a reply after marking as read makes the room unread but not the thread", () => {}); }); }); @@ -514,181 +588,181 @@ describe("Read receipts", () => { // affect only one or the other. describe("in the main timeline", () => { - test("Reacting to a message makes a room unread", () => {}); - test("Reading a reaction makes the room read", () => {}); - test("Marking a room as read after a reaction makes it read", () => {}); - test("Reacting to a message after marking as read makes the room unread", () => {}); - test("A room with a reaction is still unread after restart", () => {}); - test("A room where all reactions are read is still read after restart", () => {}); + 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", () => {}); }); describe("in threads", () => { - test("A reaction to a threaded message makes the room unread", () => {}); - test("Reading a reaction to a threaded message makes the room read", () => {}); - test("Marking a room as read after a reaction in a thread makes it read", () => {}); - test("Reacting to a thread message after marking as read makes the room unread", () => {}); - test("A room with a reaction to a threaded message is still unread after restart", () => {}); - test("A room where all reactions in threads are read is still read after restart", () => {}); + 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.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", () => { - test("A reaction to a thread root makes the room unread", () => {}); - test("Reading a reaction to a thread root makes the room read", () => {}); - test("Marking a room as read after a reaction to a thread root makes it read", () => {}); - test("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {}); + it.skip("A reaction to a thread root makes the room unread", () => {}); + 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", () => {}); }); }); describe("redactions", () => { describe("in the main timeline", () => { // One of the following two must be right: - test("Redacting the message pointed to by my receipt leaves the room read", () => {}); - test("Redacting a message after it was read makes the room unread", () => {}); - - test("Reading an unread room after a redaction of the latest message makes it read", () => {}); - test("Reading an unread room after a redaction of an older message makes it read", () => {}); - test("Marking an unread room as read after a redaction makes it read", () => {}); - test("Sending and redacting a message after marking the room as read makes it unread", () => {}); - test("?? Redacting a message after marking the room as read makes it unread", () => {}); - test("Reacting to a redacted message leaves the room read", () => {}); - test("Editing a redacted message leaves the room read", () => {}); - - test("?? Reading a reaction to a redacted message marks the room as read", () => {}); - test("?? Reading an edit of a redacted message marks the room as read", () => {}); - test("Reading a reply to a redacted message marks the room as read", () => {}); - - test("A room with an unread redaction is still unread after restart", () => {}); - test("A room with a read redaction is still read after restart", () => {}); + 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.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", () => {}); + it.skip("Marking an unread room as read after a redaction makes it read", () => {}); + it.skip("Sending and redacting a message after marking the room as read makes it unread", () => {}); + it.skip("?? Redacting a message after marking the room as read makes it unread", () => {}); + it.skip("Reacting to a redacted message leaves the room read", () => {}); + it.skip("Editing a redacted message leaves the room read", () => {}); + + it.skip("?? Reading a reaction to a redacted message marks the room as read", () => {}); + it.skip("?? Reading an edit of a redacted message marks the room as read", () => {}); + it.skip("Reading a reply to a redacted message marks the room as read", () => {}); + + it.skip("A room with an unread redaction is still unread after restart", () => {}); + it.skip("A room with a read redaction is still read after restart", () => {}); }); describe("in threads", () => { // One of the following two must be right: - test("Redacting the threaded message pointed to by my receipt leaves the room read", () => {}); - test("Redacting a threaded message after it was read makes the room unread", () => {}); - - test("Reading an unread thread after a redaction of the latest message makes it read", () => {}); - test("Reading an unread thread after a redaction of an older message makes it read", () => {}); - test("Marking an unread thread as read after a redaction makes it read", () => {}); - test("Sending and redacting a message after marking the thread as read makes it unread", () => {}); - test("?? Redacting a message after marking the thread as read makes it unread", () => {}); - test("Reacting to a redacted message leaves the thread read", () => {}); - test("Editing a redacted message leaves the thread read", () => {}); - - test("?? Reading a reaction to a redacted message marks the thread as read", () => {}); - test("?? Reading an edit of a redacted message marks the thread as read", () => {}); - test("Reading a reply to a redacted message marks the thread as read", () => {}); - - test("A thread with an unread redaction is still unread after restart", () => {}); - test("A thread with a read redaction is still read after restart", () => {}); - test("A thread with an unread reply to a redacted message is still unread after restart", () => {}); - test("A thread with a read replt to a redacted message is still read after restart", () => {}); + 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.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", () => {}); + + 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("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.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", () => {}); }); describe("thread roots", () => { // One of the following two must be right: - test("Redacting a thread root after it was read leaves the room read", () => {}); - test("Redacting a thread root after it was read makes the room unread", () => {}); - - test("Redacting the root of an unread thread makes the room read", () => {}); - test("Sending a threaded message onto a redacted thread root leaves the room read", () => {}); - test("Reacting to a redacted thread root leaves the room read", () => {}); - test("Editing a redacted thread root leaves the room read", () => {}); - test("Replying to a redacted thread root makes the room unread", () => {}); - test("Reading a reply to a redacted thread root makes the room read", () => {}); + it.skip("Redacting a thread root after it was read leaves the room read", () => {}); + it.skip("Redacting a thread root after it was read makes the room unread", () => {}); + + it.skip("Redacting the root of an unread thread makes the room read", () => {}); + it.skip("Sending a threaded message onto a redacted thread root leaves the room read", () => {}); + it.skip("Reacting to a redacted thread root leaves the room read", () => {}); + it.skip("Editing a redacted thread root leaves the room read", () => {}); + it.skip("Replying to a redacted thread root makes the room unread", () => {}); + it.skip("Reading a reply to a redacted thread root makes the room read", () => {}); }); }); describe("messages with missing referents", () => { - test("A message in an unknown thread is not visible and the room is read", () => {}); - test("When a message's thread root appears later the thread appears and the room is unread", () => {}); - test("An edit of an unknown message is not visible and the room is read", () => {}); - test("When an edit's message appears later the edited version appears and the room is unread", () => {}); - test("A reaction to an unknown message is not visible and the room is read", () => {}); - test("When an reactions's message appears later it appears and the room is unread", () => {}); + it.skip("A message in an unknown thread is not visible and the room is read", () => {}); + it.skip("When a message's thread root appears later the thread appears and the room is unread", () => {}); + it.skip("An edit of an unknown message is not visible and the room is read", () => {}); + it.skip("When an edit's message appears later the edited version appears and the room is unread", () => {}); + it.skip("A reaction to an unknown message is not visible and the room is read", () => {}); + it.skip("When an reactions's message appears later it appears and the room is unread", () => {}); // Harder: validate that we request the messages we are missing? }); describe("receipts with missing events", () => { // Later: when we have order in receipts, we can change these tests to // make receipts still work, even when their message is not found. - test("A receipt for an unknown message does not change the state of an unread room", () => {}); - test("A receipt for an unknown message does not change the state of a read room", () => {}); - test("A threaded receipt for an unknown message does not change the state of an unread thread", () => {}); - test("A threaded receipt for an unknown message does not change the state of a read thread", () => {}); - test("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {}); - test("A threaded receipt for an unknown thread does not change the state of a read thread", () => {}); - test("A threaded receipt for a message on main does not change the state of an unread room", () => {}); - test("A threaded receipt for a message on main does not change the state of a read room", () => {}); - test("A main receipt for a message on a thread does not change the state of an unread room", () => {}); - test("A main receipt for a message on a thread does not change the state of a read room", () => {}); - test("A threaded receipt for a thread root does not mark it as read", () => {}); + it.skip("A receipt for an unknown message does not change the state of an unread room", () => {}); + it.skip("A receipt for an unknown message does not change the state of a read room", () => {}); + it.skip("A threaded receipt for an unknown message does not change the state of an unread thread", () => {}); + it.skip("A threaded receipt for an unknown message does not change the state of a read thread", () => {}); + it.skip("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {}); + it.skip("A threaded receipt for an unknown thread does not change the state of a read thread", () => {}); + it.skip("A threaded receipt for a message on main does not change the state of an unread room", () => {}); + it.skip("A threaded receipt for a message on main does not change the state of a read room", () => {}); + it.skip("A main receipt for a message on a thread does not change the state of an unread room", () => {}); + it.skip("A main receipt for a message on a thread does not change the state of a read room", () => {}); + it.skip("A threaded receipt for a thread root does not mark it as read", () => {}); // Harder: validate that we request the messages we are missing? }); describe("Message ordering", () => { describe("in the main timeline", () => { - test("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {}); - test("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {}); + it.skip("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {}); + it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {}); }); describe("in threads", () => { // These don't pass yet - we need MSC4033 - we don't even know the Sync order yet - test.skip("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {}); - test.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {}); + it.skip("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {}); + it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {}); // These pass now and should not later - we should use order from MSC4033 instead of ts // These are broken out - test("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {}); - test("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {}); - test("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {}); - test("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {}); - test("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {}); - test("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {}); + it.skip("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {}); + it.skip("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {}); + it.skip("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {}); + it.skip("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {}); + it.skip("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {}); + it.skip("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {}); }); describe("thread roots", () => { - test("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {}); - test("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); - test("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {}); - test("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); + it.skip("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {}); + it.skip("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); + it.skip("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {}); + it.skip("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); }); }); describe("Ignored events", () => { - test("If all events after receipt are unimportant, the room is read", () => {}); - test("Sending an important event after unimportant ones makes the room unread", () => {}); - test("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); + 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.skip("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); }); describe("Paging up", () => { - test("Paging up through old messages after a room is read leaves the room read", () => {}); - test("Paging up through old messages of an unread room leaves the room unread", () => {}); - test("Paging up to find old threads that were previously read leaves the room read", () => {}); - test("?? Paging up to find old threads that were never read marks the room unread", () => {}); - test("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); + it.skip("Paging up through old messages after a room is read leaves the room read", () => {}); + it.skip("Paging up through old messages of an unread room leaves the room unread", () => {}); + it.skip("Paging up to find old threads that were previously read leaves the room read", () => {}); + it.skip("?? Paging up to find old threads that were never read marks the room unread", () => {}); + it.skip("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); }); describe("Room list order", () => { - test("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {}); + it.skip("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {}); }); describe("Notifications", () => { describe("in the main timeline", () => { - test("A new message that mentions me shows a notification", () => {}); - test("Reading a notifying message reduces the notification count in the room list, space and tab", () => {}); - test("Reading the last notifying message removes the notification marker from room list, space and tab", () => {}); - test("Editing a message to mentions me shows a notification", () => {}); - test("Reading the last notifying edited message removes the notification marker", () => {}); - test("Redacting a notifying message removes the notification marker", () => {}); + it.skip("A new message that mentions me shows a notification", () => {}); + it.skip("Reading a notifying message reduces the notification count in the room list, space and tab", () => {}); + it.skip("Reading the last notifying message removes the notification marker from room list, space and tab", () => {}); + it.skip("Editing a message to mentions me shows a notification", () => {}); + it.skip("Reading the last notifying edited message removes the notification marker", () => {}); + it.skip("Redacting a notifying message removes the notification marker", () => {}); }); describe("in threads", () => { - test("A new threaded message that mentions me shows a notification", () => {}); - test("Reading a notifying threaded message removes the notification count", () => {}); - test("Notification count remains steady when reading threads that contain seen notifications", () => {}); - test("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {}); - test("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {}); - test("Redacting a notifying threaded message removes the notification marker", () => {}); + it.skip("A new threaded message that mentions me shows a notification", () => {}); + it.skip("Reading a notifying threaded message removes the notification count", () => {}); + it.skip("Notification count remains steady when reading threads that contain seen notifications", () => {}); + it.skip("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {}); + it.skip("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {}); + it.skip("Redacting a notifying threaded message removes the notification marker", () => {}); }); }); }); diff --git a/cypress/support/views.ts b/cypress/support/views.ts index d5bf9179896..a5936279bad 100644 --- a/cypress/support/views.ts +++ b/cypress/support/views.ts @@ -63,7 +63,10 @@ declare global { } Cypress.Commands.add("viewRoomByName", (name: string): Chainable> => { - return cy.findByRole("treeitem", { name: name }).should("have.class", "mx_RoomTile").click(); + return cy + .findByRole("treeitem", { name: new RegExp("^" + name) }) + .should("have.class", "mx_RoomTile") + .click(); }); Cypress.Commands.add("viewRoomById", (id: string): void => { From 1689c97244cb77db6de5d0bf40e981d026998d8d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 31 Jul 2023 17:52:28 +0100 Subject: [PATCH 09/37] Wire up additional tests --- .../e2e/read-receipts/read-receipts.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index e9c42385bb9..173e00aa42a 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -496,7 +496,23 @@ describe("Read receipts", () => { assertRead(room2); }); it.skip("Marking a room with unread threads as read makes it read", () => {}); - it.skip("Sending a new thread message after marking as read makes it unread", () => {}); + it("Sending a new thread message after marking as read makes it unread", () => { + // Given a thread exists + goTo(room1); + sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + + // When I read the main timeline + goTo(room2); + + // And the thread + openThread("Msg1"); + + goTo(room1); + // Receive additional response to thread whilst not looking at room + sendMessages(room2, [threadedOff("Msg1", "Resp3")]); + + assertUnread(room2); + }); it.skip("A room with a new threaded message is still unread after restart", () => {}); it.skip("A room where all threaded messages are read is still read after restart", () => {}); }); From 9072afa9950dd133b5fe892e8c1474bfa7f74fef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Aug 2023 10:27:02 +0100 Subject: [PATCH 10/37] Wire up more tests --- .../e2e/read-receipts/read-receipts.spec.ts | 91 +++++++++++++++++-- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 173e00aa42a..cdc261b12c7 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -20,6 +20,7 @@ import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; import type { Room } from "matrix-js-sdk/src/matrix"; +import type { IndexedDBStore } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import Chainable = Cypress.Chainable; @@ -446,14 +447,23 @@ describe("Read receipts", () => { })(); } + function getRoomListTile(room: string) { + return cy.findByRole("treeitem", { name: new RegExp("^" + room) }); + } + + function markAsRead(room: string) { + getRoomListTile(room).rightclick(); + cy.findByText("Mark as read").click(); + } + function assertRead(room: string) { - return cy.findByRole("treeitem", { name: new RegExp("^" + room) }).within(() => { + return getRoomListTile(room).within(() => { cy.get(".mx_NotificationBadge_count").should("not.exist"); }); } function assertUnread(room: string) { - return cy.findByRole("treeitem", { name: new RegExp("^" + room) }).within(() => { + return getRoomListTile(room).within(() => { cy.get(".mx_NotificationBadge_count").should("exist"); }); } @@ -463,13 +473,76 @@ describe("Read receipts", () => { describe("new messages", () => { describe("in the main timeline", () => { - it.skip("Sending a message makes a room unread", () => {}); - it.skip("Reading latest message makes the room read", () => {}); + it("Sending a message makes a room unread", () => { + goTo(room1); + assertRead(room2); + + sendMessages(room2, ["Msg1"]); + assertUnread(room2); + }); + it("Reading latest message makes the room read", () => { + goTo(room1); + assertRead(room2); + sendMessages(room2, ["Msg1"]); + assertUnread(room2); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + }); it.skip("Reading an older message leaves the room unread", () => {}); - it.skip("Marking a room as read makes it read", () => {}); - it.skip("Sending a new message after marking as read makes it unread", () => {}); - it.skip("A room with a new message is still unread after restart", () => {}); - it.skip("A room where all messages are read is still read after restart", () => {}); + it("Marking a room as read makes it read", () => { + goTo(room1); + assertRead(room2); + sendMessages(room2, ["Msg1"]); + assertUnread(room2); + + markAsRead(room2); + assertRead(room2); + }); + it("Sending a new message after marking as read makes it unread", () => { + goTo(room1); + assertRead(room2); + sendMessages(room2, ["Msg1"]); + assertUnread(room2); + + markAsRead(room2); + assertRead(room2); + + sendMessages(room2, ["Msg2"]); + assertUnread(room2); + }); + it("A room with a new message is still unread after restart", () => { + goTo(room1); + assertRead(room2); + sendMessages(room2, ["Msg1"]); + assertUnread(room2); + + cy.getClient().then((cli) => { + // @ts-ignore + return (cli.store as IndexedDBStore).reallySave(); + }); + + cy.reload(); + assertUnread(room2); + }); + it("A room where all messages are read is still read after restart", () => { + goTo(room1); + assertRead(room2); + sendMessages(room2, ["Msg1"]); + assertUnread(room2); + + markAsRead(room2); + assertRead(room2); + + cy.getClient().then((cli) => { + // @ts-ignore + return (cli.store as IndexedDBStore).reallySave(); + }); + + cy.reload(); + assertRead(room2); + }); }); describe("in threads", () => { @@ -570,7 +643,7 @@ describe("Read receipts", () => { describe("thread roots", () => { it.skip("An edit of a thread root makes the room unread", () => {}); - it("Reading an edit of a thread root makes the room read", () => { + it.skip("Reading an edit of a thread root makes the room read", () => { // Given a fully-read thread exists goTo(room2); sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); From 1921a2be63dcacf896f3c9b4a168de92c33c8f30 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Aug 2023 10:30:49 +0100 Subject: [PATCH 11/37] Tidy --- .../e2e/read-receipts/read-receipts.spec.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index cdc261b12c7..e952223967c 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -468,6 +468,14 @@ describe("Read receipts", () => { }); } + function saveAndReload() { + cy.getClient().then((cli) => { + // @ts-ignore + return (cli.store as IndexedDBStore).reallySave(); + }); + cy.reload(); + } + const room1 = selectedRoomName; const room2 = otherRoomName; @@ -518,12 +526,7 @@ describe("Read receipts", () => { sendMessages(room2, ["Msg1"]); assertUnread(room2); - cy.getClient().then((cli) => { - // @ts-ignore - return (cli.store as IndexedDBStore).reallySave(); - }); - - cy.reload(); + saveAndReload(); assertUnread(room2); }); it("A room where all messages are read is still read after restart", () => { @@ -535,12 +538,7 @@ describe("Read receipts", () => { markAsRead(room2); assertRead(room2); - cy.getClient().then((cli) => { - // @ts-ignore - return (cli.store as IndexedDBStore).reallySave(); - }); - - cy.reload(); + saveAndReload(); assertRead(room2); }); }); From 39d4924bff35861a1fb37405d13e596ebe4052f9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Aug 2023 10:36:38 +0100 Subject: [PATCH 12/37] Wire up more tests --- .../e2e/read-receipts/read-receipts.spec.ts | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index e952223967c..98a40456814 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -566,7 +566,17 @@ describe("Read receipts", () => { openThread("Msg1"); assertRead(room2); }); - it.skip("Marking a room with unread threads as read makes it read", () => {}); + it("Marking a room with unread threads as read makes it read", () => { + goTo(room1); + sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2); // (Sanity) + + goTo(room2); + assertUnread(room2); + + markAsRead(room2); + assertRead(room2); + }); it("Sending a new thread message after marking as read makes it unread", () => { // Given a thread exists goTo(room1); @@ -584,8 +594,44 @@ describe("Read receipts", () => { assertUnread(room2); }); - it.skip("A room with a new threaded message is still unread after restart", () => {}); - it.skip("A room where all threaded messages are read is still read after restart", () => {}); + it("A room with a new threaded message is still unread after restart", () => { + // Given a thread exists + goTo(room1); + sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does not appear unread + assertUnread(room2); + + saveAndReload(); + assertUnread(room2); + + // Until we open the thread + openThread("Msg1"); + assertRead(room2); + }); + it("A room where all threaded messages are read is still read after restart", () => { + // Given a thread exists + goTo(room1); + sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does not appear unread + assertUnread(room2); + + // Until we open the thread + openThread("Msg1"); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); }); describe("thread roots", () => { From 11174bbf20c631e34296f24a25761ecfc4814bff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Aug 2023 11:50:56 +0100 Subject: [PATCH 13/37] Wire up more tests --- .../e2e/read-receipts/read-receipts.spec.ts | 140 ++++++++++++++---- 1 file changed, 111 insertions(+), 29 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 98a40456814..7d9b439bf79 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -385,7 +385,7 @@ describe("Read receipts", () => { } // Sends messages into given room as a bot - function sendMessages(room: string, messages: Message[]) { + function receiveMessages(room: string, messages: Message[]) { findRoomByName(room).then(async ({ roomId }) => { const room = bot.getRoom(roomId); for (const message of messages) { @@ -468,6 +468,34 @@ describe("Read receipts", () => { }); } + function openThreadList() { + cy.findByTestId("threadsButton").then((button) => { + if (button?.attr("aria-current") !== "true") { + button.trigger("click"); + } + }); + Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); + } + + function getThreadListTile(rootMessage: string) { + openThreadList(); + return cy.get(".mx_ThreadPanel").within(() => { + return cy.contains(".mx_EventTile_body", rootMessage).closest("li"); + }); + } + + function assertReadThread(rootMessage: string) { + return getThreadListTile(rootMessage).within(() => { + cy.get(".mx_NotificationBadge").should("not.exist"); + }); + } + + function assertUnreadThread(rootMessage: string) { + return getThreadListTile(rootMessage).within(() => { + cy.get(".mx_NotificationBadge").should("exist"); + }); + } + function saveAndReload() { cy.getClient().then((cli) => { // @ts-ignore @@ -485,13 +513,13 @@ describe("Read receipts", () => { goTo(room1); assertRead(room2); - sendMessages(room2, ["Msg1"]); + receiveMessages(room2, ["Msg1"]); assertUnread(room2); }); it("Reading latest message makes the room read", () => { goTo(room1); assertRead(room2); - sendMessages(room2, ["Msg1"]); + receiveMessages(room2, ["Msg1"]); assertUnread(room2); // When I read the main timeline @@ -502,7 +530,7 @@ describe("Read receipts", () => { it("Marking a room as read makes it read", () => { goTo(room1); assertRead(room2); - sendMessages(room2, ["Msg1"]); + receiveMessages(room2, ["Msg1"]); assertUnread(room2); markAsRead(room2); @@ -511,19 +539,19 @@ describe("Read receipts", () => { it("Sending a new message after marking as read makes it unread", () => { goTo(room1); assertRead(room2); - sendMessages(room2, ["Msg1"]); + receiveMessages(room2, ["Msg1"]); assertUnread(room2); markAsRead(room2); assertRead(room2); - sendMessages(room2, ["Msg2"]); + receiveMessages(room2, ["Msg2"]); assertUnread(room2); }); it("A room with a new message is still unread after restart", () => { goTo(room1); assertRead(room2); - sendMessages(room2, ["Msg1"]); + receiveMessages(room2, ["Msg1"]); assertUnread(room2); saveAndReload(); @@ -532,7 +560,7 @@ describe("Read receipts", () => { it("A room where all messages are read is still read after restart", () => { goTo(room1); assertRead(room2); - sendMessages(room2, ["Msg1"]); + receiveMessages(room2, ["Msg1"]); assertUnread(room2); markAsRead(room2); @@ -544,31 +572,73 @@ describe("Read receipts", () => { }); describe("in threads", () => { - it.skip("Sending a message makes a room unread", () => {}); - it.skip("Reading the last threaded message makes the room read", () => {}); - it.skip("Reading a thread message makes the thread read", () => {}); - it.skip("Reading an older thread message (via permalink) leaves the thread unread", () => {}); - it.skip("Reading only one thread's message does not make the room read", () => {}); - it.skip("Reading only one thread's message make that thread read but not others", () => {}); - it("Reading the main timeline does not mark a thread message as read", () => { + it("Sending a message makes a room unread", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2); + goTo(room2); + + assertRead(room2); + goTo(room1); + + receiveMessages(room2, [threadedOff("Msg1", "Resp1")]); + assertUnread(room2); + }); + it("Reading the last threaded message makes the room read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2); + goTo(room2); + + openThread("Msg1"); + assertRead(room2); + }); + it("Reading a thread message makes the thread read", () => { // Given a thread exists goTo(room1); - sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); assertUnread(room2); // (Sanity) // When I read the main timeline goTo(room2); - // Then room does not appear unread + // Then room does appear unread assertUnread(room2); // Until we open the thread openThread("Msg1"); + assertReadThread("Msg1"); assertRead(room2); }); + it.skip("Reading an older thread message (via permalink) leaves the thread unread", () => {}); + it("Reading only one thread's message does not make the room read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]); + assertUnread(room2); + goTo(room2); + assertUnread(room2); + + openThread("Msg1"); + assertUnread(room2); + }); + it.skip("Reading only one thread's message make that thread read but not others", () => {}); + it("Reading the main timeline does not mark a thread message as read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does appear unread + assertUnread(room2); + }); it("Marking a room with unread threads as read makes it read", () => { goTo(room1); - sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); assertUnread(room2); // (Sanity) goTo(room2); @@ -580,7 +650,7 @@ describe("Read receipts", () => { it("Sending a new thread message after marking as read makes it unread", () => { // Given a thread exists goTo(room1); - sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); // When I read the main timeline goTo(room2); @@ -590,20 +660,20 @@ describe("Read receipts", () => { goTo(room1); // Receive additional response to thread whilst not looking at room - sendMessages(room2, [threadedOff("Msg1", "Resp3")]); + receiveMessages(room2, [threadedOff("Msg1", "Resp3")]); assertUnread(room2); }); it("A room with a new threaded message is still unread after restart", () => { // Given a thread exists goTo(room1); - sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); assertUnread(room2); // (Sanity) // When I read the main timeline goTo(room2); - // Then room does not appear unread + // Then room does appear unread assertUnread(room2); saveAndReload(); @@ -616,13 +686,13 @@ describe("Read receipts", () => { it("A room where all threaded messages are read is still read after restart", () => { // Given a thread exists goTo(room1); - sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); assertUnread(room2); // (Sanity) // When I read the main timeline goTo(room2); - // Then room does not appear unread + // Then room does appear unread assertUnread(room2); // Until we open the thread @@ -635,7 +705,19 @@ describe("Read receipts", () => { }); describe("thread roots", () => { - it.skip("Reading a thread root does not mark the thread as read", () => {}); + it("Reading a thread root does not mark the thread as read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does appear unread + assertUnread(room2); + assertUnreadThread("Msg1"); + }); it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); it.skip("Creating a new thread based on a reply makes the room unread", () => {}); it.skip("Reading a thread whose root is a reply makes the room read", () => {}); @@ -649,7 +731,7 @@ describe("Read receipts", () => { goTo(room1); // When an edit appears in the room - sendMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); + receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); // Then it becomes unread assertUnread(room2); @@ -657,7 +739,7 @@ describe("Read receipts", () => { it.skip("Reading an edit makes the room read", () => { // Given an edit made a room unread goTo(room1); - sendMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); + receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); assertUnread(room2); // (Sanity) // When I read it @@ -690,13 +772,13 @@ describe("Read receipts", () => { it.skip("Reading an edit of a thread root makes the room read", () => { // Given a fully-read thread exists goTo(room2); - sendMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); openThread("Msg1"); goTo(room1); assertRead(room2); // When the thread root is edited - sendMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); // And I read that edit goTo(room2); From 4f8d5291c53711ca6403246b5e463a0302f275c5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Aug 2023 16:03:52 +0100 Subject: [PATCH 14/37] Wire up more tests --- .../e2e/read-receipts/read-receipts.spec.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 7d9b439bf79..4f8402a32df 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -429,6 +429,24 @@ describe("Read receipts", () => { })(); } + function replyTo(targetMessage: string, newMessage: string): MessageSpec { + return new (class extends MessageSpec { + public async getContent(room: Room): Promise> { + const ev = await getMessage(room, targetMessage); + + return { + "msgtype": "m.text", + "body": newMessage, + "m.relates_to": { + "m.in_reply_to": { + event_id: ev.getId(), + }, + }, + }; + } + })(); + } + function threadedOff(rootMessage: string, newMessage: string): MessageSpec { return new (class extends MessageSpec { public async getContent(room: Room): Promise> { @@ -719,8 +737,23 @@ describe("Read receipts", () => { assertUnreadThread("Msg1"); }); it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); - it.skip("Creating a new thread based on a reply makes the room unread", () => {}); - it.skip("Reading a thread whose root is a reply makes the room read", () => {}); + it("Creating a new thread based on a reply makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); + assertUnread(room2); + }); + it("Reading a thread whose root is a reply makes the room read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); + assertUnread(room2); + + goTo(room2); + assertUnread(room2); + assertUnreadThread("Reply1"); + + openThread("Reply1"); + assertRead(room2); + }); }); }); From 6c8a9a67c18497dc8cb8920e3852248ec65b31c4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 09:04:57 +0100 Subject: [PATCH 15/37] Wire up more tests --- .../e2e/read-receipts/read-receipts.spec.ts | 118 ++++++++++-------- 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 4f8402a32df..59fa4b2dd85 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -480,26 +480,31 @@ describe("Read receipts", () => { }); } - function assertUnread(room: string) { + function assertUnread(room: string, count: number | ".") { return getRoomListTile(room).within(() => { - cy.get(".mx_NotificationBadge_count").should("exist"); + if (count === ".") { + cy.get(".mx_NotificationBadge_dot").should("exist"); + } else { + cy.get(".mx_NotificationBadge_count").should("have.text", count); + } }); } function openThreadList() { - cy.findByTestId("threadsButton").then((button) => { - if (button?.attr("aria-current") !== "true") { - button.trigger("click"); - } - }); - Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); + cy.findByTestId("threadsButton") + .then((button) => { + if (button?.attr("aria-current") !== "true") { + button.trigger("click"); + } + }) + .then(() => { + Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); + }); } function getThreadListTile(rootMessage: string) { openThreadList(); - return cy.get(".mx_ThreadPanel").within(() => { - return cy.contains(".mx_EventTile_body", rootMessage).closest("li"); - }); + return cy.contains(".mx_ThreadPanel .mx_EventTile_body", rootMessage).closest("li"); } function assertReadThread(rootMessage: string) { @@ -520,6 +525,8 @@ describe("Read receipts", () => { return (cli.store as IndexedDBStore).reallySave(); }); cy.reload(); + // Wait for the app to reload + cy.get(".mx_RoomView").should("exist"); } const room1 = selectedRoomName; @@ -532,13 +539,13 @@ describe("Read receipts", () => { assertRead(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); }); it("Reading latest message makes the room read", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); // When I read the main timeline goTo(room2); @@ -549,7 +556,7 @@ describe("Read receipts", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); markAsRead(room2); assertRead(room2); @@ -558,28 +565,28 @@ describe("Read receipts", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); markAsRead(room2); assertRead(room2); receiveMessages(room2, ["Msg2"]); - assertUnread(room2); + assertUnread(room2, 1); }); it("A room with a new message is still unread after restart", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); saveAndReload(); - assertUnread(room2); + assertUnread(room2, 1); }); it("A room where all messages are read is still read after restart", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); markAsRead(room2); assertRead(room2); @@ -594,20 +601,20 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1"]); - assertUnread(room2); + assertUnread(room2, 1); goTo(room2); assertRead(room2); goTo(room1); receiveMessages(room2, [threadedOff("Msg1", "Resp1")]); - assertUnread(room2); + assertUnread(room2, 1); }); it("Reading the last threaded message makes the room read", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2); + assertUnread(room2, 1); goTo(room2); openThread("Msg1"); @@ -617,13 +624,13 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2); // (Sanity) + assertUnread(room2, 2); // (Sanity) // When I read the main timeline goTo(room2); // Then room does appear unread - assertUnread(room2); + assertUnread(room2, 2); // Until we open the thread openThread("Msg1"); @@ -634,33 +641,46 @@ describe("Read receipts", () => { it("Reading only one thread's message does not make the room read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]); - assertUnread(room2); + assertUnread(room2, 4); + goTo(room2); + assertUnread(room2, 4); + + openThread("Msg1"); + assertUnread(room2, 1); + }); + it("Reading only one thread's message make that thread read but not others", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2", threadedOff("Msg1", "Resp1"), threadedOff("Msg2", "Resp2")]); + assertUnread(room2, 4); // (Sanity) + + // When I read the main timeline goTo(room2); - assertUnread(room2); + + assertUnread(room2, 2); + assertUnreadThread("Msg1"); + assertUnreadThread("Msg2"); openThread("Msg1"); - assertUnread(room2); + assertReadThread("Msg1"); + assertUnreadThread("Msg2"); }); - it.skip("Reading only one thread's message make that thread read but not others", () => {}); it("Reading the main timeline does not mark a thread message as read", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2); // (Sanity) + assertUnread(room2, 3); // (Sanity) // When I read the main timeline goTo(room2); - // Then room does appear unread - assertUnread(room2); + assertUnread(room2, 2); + // Then thread does appear unread + assertUnreadThread("Msg1"); }); - it("Marking a room with unread threads as read makes it read", () => { + it.skip("Marking a room with unread threads as read makes it read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2); // (Sanity) - - goTo(room2); - assertUnread(room2); + assertUnread(room2, 2); // (Sanity) markAsRead(room2); assertRead(room2); @@ -680,22 +700,22 @@ describe("Read receipts", () => { // Receive additional response to thread whilst not looking at room receiveMessages(room2, [threadedOff("Msg1", "Resp3")]); - assertUnread(room2); + assertUnread(room2, 1); }); it("A room with a new threaded message is still unread after restart", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2); // (Sanity) + assertUnread(room2, 2); // (Sanity) // When I read the main timeline goTo(room2); // Then room does appear unread - assertUnread(room2); + assertUnread(room2, 2); saveAndReload(); - assertUnread(room2); + assertUnread(room2, 2); // Until we open the thread openThread("Msg1"); @@ -705,13 +725,13 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2); // (Sanity) + assertUnread(room2, 3); // (Sanity) // When I read the main timeline goTo(room2); // Then room does appear unread - assertUnread(room2); + assertUnread(room2, 2); // Until we open the thread openThread("Msg1"); @@ -727,28 +747,28 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2); // (Sanity) + assertUnread(room2, 1); // (Sanity) // When I read the main timeline goTo(room2); // Then room does appear unread - assertUnread(room2); + assertUnread(room2, 2); assertUnreadThread("Msg1"); }); it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); it("Creating a new thread based on a reply makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); - assertUnread(room2); + assertUnread(room2, 3); }); it("Reading a thread whose root is a reply makes the room read", () => { goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); - assertUnread(room2); + assertUnread(room2, 3); goTo(room2); - assertUnread(room2); + assertUnread(room2, 1); assertUnreadThread("Reply1"); openThread("Reply1"); @@ -767,13 +787,13 @@ describe("Read receipts", () => { receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); // Then it becomes unread - assertUnread(room2); + assertUnread(room2, 1); }); it.skip("Reading an edit makes the room read", () => { // Given an edit made a room unread goTo(room1); receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); - assertUnread(room2); // (Sanity) + assertUnread(room2, 1); // (Sanity) // When I read it goTo(room2); From abfec3ff2cde7130c133b22e7f3beb64a6d5b05e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 09:09:19 +0100 Subject: [PATCH 16/37] Mute browser --- cypress.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cypress.config.ts b/cypress.config.ts index bc247638527..df0e93195a5 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -25,6 +25,15 @@ export default defineConfig({ chromeWebSecurity: false, e2e: { setupNodeEvents(on, config) { + // Mute browser to improve local development experience + on("before:browser:launch", (browser, launchOptions) => { + if (browser.family === "chromium") { + launchOptions.args.push("--mute-audio"); + } + + return launchOptions; + }); + return require("./cypress/plugins/index.ts").default(on, config); }, baseUrl: "http://localhost:8080", From 000d32799eefdcda5c71605cad6231109415cb1f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 09:13:21 +0100 Subject: [PATCH 17/37] Silence electron warnings --- cypress.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.config.ts b/cypress.config.ts index df0e93195a5..c4b307bdccd 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ setupNodeEvents(on, config) { // Mute browser to improve local development experience on("before:browser:launch", (browser, launchOptions) => { - if (browser.family === "chromium") { + if (browser.name === "chrome" || browser.name === "chromium") { launchOptions.args.push("--mute-audio"); } From 2028e5167a4edd36981f25f6ec15def6464059e3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 09:21:07 +0100 Subject: [PATCH 18/37] Iterate --- cypress/e2e/read-receipts/read-receipts.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 59fa4b2dd85..c513445d090 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -677,10 +677,11 @@ describe("Read receipts", () => { // Then thread does appear unread assertUnreadThread("Msg1"); }); + // XXX: this failure seems legit it.skip("Marking a room with unread threads as read makes it read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) + assertUnread(room2, 3); // (Sanity) markAsRead(room2); assertRead(room2); From 6318375510e88ecacccd98b6e2b7e2e2b7421e3d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 09:21:29 +0100 Subject: [PATCH 19/37] revert --- cypress.config.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index c4b307bdccd..bc247638527 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -25,15 +25,6 @@ export default defineConfig({ chromeWebSecurity: false, e2e: { setupNodeEvents(on, config) { - // Mute browser to improve local development experience - on("before:browser:launch", (browser, launchOptions) => { - if (browser.name === "chrome" || browser.name === "chromium") { - launchOptions.args.push("--mute-audio"); - } - - return launchOptions; - }); - return require("./cypress/plugins/index.ts").default(on, config); }, baseUrl: "http://localhost:8080", From a2a195cbd234fa6ff4c35ffb0298755c4a500342 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 09:36:52 +0100 Subject: [PATCH 20/37] Wire up more tests --- .../e2e/read-receipts/read-receipts.spec.ts | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index c513445d090..b62c8908dc1 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -780,32 +780,63 @@ describe("Read receipts", () => { describe("editing messages", () => { describe("in the main timeline", () => { - it.skip("Editing a message makes a room unread", () => { + it("Editing a message makes a room unread", () => { // Given I am not in the room goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + // When an edit appears in the room - receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); // Then it becomes unread assertUnread(room2, 1); }); - it.skip("Reading an edit makes the room read", () => { - // Given an edit made a room unread + it("Reading an edit makes the room read", () => { goTo(room1); - receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]); - assertUnread(room2, 1); // (Sanity) + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + assertUnread(room2, 1); // When I read it goTo(room2); - // Then the room becomes unread and stays unread + // Then the room becomes read and stays read assertRead(room2); goTo(room1); assertRead(room2); }); - it.skip("Marking a room as read after an edit makes it read", () => {}); - it.skip("Editing a message after marking as read makes the room unread", () => {}); + it("Marking a room as read after an edit makes it read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + assertUnread(room2, 1); + + // When I mark it as read + markAsRead(room2); + + // Then the room becomes read + assertRead(room2); + }); + it("Editing a message after marking as read makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + // When I mark it as read + markAsRead(room2); + + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + + // Then the room becomes unread + assertUnread(room2, 1); + }); it.skip("Editing a reply after reading it makes the room unread", () => {}); it.skip("Editing a reply after marking as read makes the room unread", () => {}); it.skip("A room with an edit is still unread after restart", () => {}); From 641d549688ccf2b0c7f580ce6800f3fa30a77c59 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 10:12:01 +0100 Subject: [PATCH 21/37] Try to stabilise tests --- cypress/e2e/read-receipts/read-receipts.spec.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index b62c8908dc1..296b923daff 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -491,15 +491,9 @@ describe("Read receipts", () => { } function openThreadList() { - cy.findByTestId("threadsButton") - .then((button) => { - if (button?.attr("aria-current") !== "true") { - button.trigger("click"); - } - }) - .then(() => { - Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); - }); + // We don't use `cy.get` here as these are inherently conditional to deal more generically with more app states + Cypress.$('[data-testid="threadsButton"][aria-current=false]')?.trigger("click"); + Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); } function getThreadListTile(rootMessage: string) { From 401db22b7c0400c99e7eb7c654efab55ad379006 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2023 11:45:53 +0100 Subject: [PATCH 22/37] Try to stabilise tests --- cypress/e2e/read-receipts/read-receipts.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 296b923daff..af74da4f53f 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -491,8 +491,13 @@ describe("Read receipts", () => { } function openThreadList() { - // We don't use `cy.get` here as these are inherently conditional to deal more generically with more app states - Cypress.$('[data-testid="threadsButton"][aria-current=false]')?.trigger("click"); + cy.findByTestId("threadsButton").then((button) => { + if (button?.attr("aria-current") !== "true") { + button.trigger("click"); + } + }); + cy.get(".mx_ThreadPanel").should("exist"); + // If the Threads back button is present then click it, the threads button can open either threads list or thread panel Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); } From 988baeeaecca370c07647eb95600727bef045e0c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Aug 2023 15:08:54 +0100 Subject: [PATCH 23/37] Validate that the notification dot is missing as well as the count --- cypress/e2e/read-receipts/read-receipts.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index af74da4f53f..58dfd6e3c53 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -476,6 +476,7 @@ describe("Read receipts", () => { function assertRead(room: string) { return getRoomListTile(room).within(() => { + cy.get(".mx_NotificationBadge_dot").should("not.exist"); cy.get(".mx_NotificationBadge_count").should("not.exist"); }); } From cbf04d29729ce7a4a2d94a7b76a9c39e37b26471 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Aug 2023 15:09:18 +0100 Subject: [PATCH 24/37] Skip a test that is failing for unknown reasons --- cypress/e2e/read-receipts/read-receipts.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 58dfd6e3c53..05055097fda 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -648,7 +648,8 @@ describe("Read receipts", () => { openThread("Msg1"); assertUnread(room2, 1); }); - it("Reading only one thread's message make that thread read but not others", () => { + // XXX: Fails, but looks like it is working in the UI - needs investigation + it.skip("Reading only one thread's message makes that thread read but not others", () => { goTo(room1); receiveMessages(room2, ["Msg1", "Msg2", threadedOff("Msg1", "Resp1"), threadedOff("Msg2", "Resp2")]); assertUnread(room2, 4); // (Sanity) From d4ea1a7a18261557d1552c70892a290c9dc39209 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Aug 2023 15:10:32 +0100 Subject: [PATCH 25/37] Use markAsRead in 'marking as read' test and add related test --- .../e2e/read-receipts/read-receipts.spec.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 05055097fda..101f493157c 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -678,7 +678,7 @@ describe("Read receipts", () => { // Then thread does appear unread assertUnreadThread("Msg1"); }); - // XXX: this failure seems legit + // XXX: fails because the room is still "bold" even though the notification counts all disappear it.skip("Marking a room with unread threads as read makes it read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); @@ -687,21 +687,38 @@ describe("Read receipts", () => { markAsRead(room2); assertRead(room2); }); - it("Sending a new thread message after marking as read makes it unread", () => { + // XXX: fails for the same reason as "Marking a room with unread threads as read makes it read" + it.skip("Sending a new thread message after marking as read makes it unread", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - // When I read the main timeline - goTo(room2); + // When I mark the room as read + markAsRead(room2); + assertRead(room2); - // And the thread - openThread("Msg1"); + // Then another message appears in the thread + receiveMessages(room2, [threadedOff("Msg1", "Resp3")]); + // Then the room becomes unread + assertUnread(room2, 1); + }); + // XXX: fails for the same reason as "Marking a room with unread threads as read makes it read" + it.skip("Sending a new different-thread message after marking as read makes it unread", () => { + // Given 2 threads exist, and Thread2 has the latest message in it goTo(room1); - // Receive additional response to thread whilst not looking at room - receiveMessages(room2, [threadedOff("Msg1", "Resp3")]); + receiveMessages(room2, ["Thread1", "Thread2", threadedOff("Thread1", "t1a")]); + assertUnread(room2, 3); + receiveMessages(room2, [threadedOff("Thread2", "t2a")]); + + // When I mark the room as read (making an unthreaded receipt for t2a) + markAsRead(room2); + assertRead(room2); + // Then another message appears in the other thread + receiveMessages(room2, [threadedOff("Thread1", "t1b")]); + + // Then the room becomes unread assertUnread(room2, 1); }); it("A room with a new threaded message is still unread after restart", () => { From cf91198bb1f60d18bf3180200cc5fd3b4680a997 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Aug 2023 15:12:07 +0100 Subject: [PATCH 26/37] Fix incorrect comment --- cypress/e2e/read-receipts/read-receipts.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 101f493157c..8a58c3cdfe4 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -886,7 +886,7 @@ describe("Read receipts", () => { // And I read that edit goTo(room2); - // Then the room becomes unread and stays unread + // Then the room becomes read and stays read assertRead(room2); goTo(room1); assertRead(room2); From d5a29107152460e00b04758a6dbd97b2d1ec124a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Aug 2023 12:46:10 +0100 Subject: [PATCH 27/37] Extract tests to their own suite --- cypress/e2e/read-receipts/high-level.spec.ts | 801 ++++++++++++++++++ .../e2e/read-receipts/read-receipts.spec.ts | 729 ---------------- 2 files changed, 801 insertions(+), 729 deletions(-) create mode 100644 cypress/e2e/read-receipts/high-level.spec.ts diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts new file mode 100644 index 00000000000..f45edf8e575 --- /dev/null +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -0,0 +1,801 @@ +/* +Copyright 2023 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 type { MatrixClient, MatrixEvent, Room, IndexedDBStore } from "matrix-js-sdk/src/matrix"; +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import Chainable = Cypress.Chainable; + +describe("Read receipts", () => { + const userName = "Mae"; + const botName = "Other User"; + const selectedRoomName = "Selected Room"; + const otherRoomName = "Other Room"; + + let homeserver: HomeserverInstance; + let otherRoomId: string; + let selectedRoomId: string; + let bot: MatrixClient | undefined; + + beforeEach(() => { + /* + * Create 2 rooms: + * + * - Selected room - this one is clicked in the UI + * - Other room - this one contains the bot, which will send events so + * we can check its unread state. + */ + cy.startHomeserver("default").then((data) => { + homeserver = data; + cy.initTestUser(homeserver, userName) + .then(() => { + cy.createRoom({ name: selectedRoomName }).then((createdRoomId) => { + selectedRoomId = createdRoomId; + }); + }) + .then(() => { + cy.createRoom({ name: otherRoomName }).then((createdRoomId) => { + otherRoomId = createdRoomId; + }); + }) + .then(() => { + cy.getBot(homeserver, { displayName: botName }).then((botClient) => { + bot = botClient; + }); + }) + .then(() => { + // Invite the bot to Other room + cy.inviteUser(otherRoomId, bot.getUserId()); + cy.visit("/#/room/" + otherRoomId); + cy.findByText(botName + " joined the room").should("exist"); + + // Then go into Selected room + cy.visit("/#/room/" + selectedRoomId); + }); + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + abstract class MessageSpec { + public abstract getContent(room: Room): Promise>; + } + + type Message = string | MessageSpec; + + function goTo(room: string) { + cy.viewRoomByName(room); + } + + function findRoomByName(room: string): Chainable { + return cy.getClient().then((cli) => { + return cli.getRooms().find((r) => r.name === room); + }); + } + + function openThread(rootMessage: string) { + cy.get(".mx_RoomView_body").within(() => { + cy.contains(".mx_EventTile[data-scroll-tokens]", rootMessage) + .realHover() + .findByRole("button", { name: "Reply in thread" }) + .click(); + }); + cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); + } + + // Sends messages into given room as a bot + function receiveMessages(room: string, messages: Message[]) { + findRoomByName(room).then(async ({ roomId }) => { + const room = bot.getRoom(roomId); + for (const message of messages) { + if (typeof message === "string") { + await bot.sendTextMessage(roomId, message); + } else { + await bot.sendMessage(roomId, await message.getContent(room)); + } + } + }); + } + + async function getMessage(room: Room, message: string): Promise { + const ev = room.timeline.find((e) => e.getContent().body === message); + if (ev) return ev; + + return new Promise((resolve) => { + room.on("Room.timeline" as any, (ev: MatrixEvent) => { + if (ev.getContent().body === message) { + resolve(ev); + } + }); + }); + } + + function editOf(originalMessage: string, newMessage: string): MessageSpec { + return new (class extends MessageSpec { + public async getContent(room: Room): Promise> { + const ev = await getMessage(room, originalMessage); + + const content = ev.getContent(); + return { + "msgtype": content.msgtype, + "body": `* ${newMessage}`, + "m.new_content": { + msgtype: content.msgtype, + body: newMessage, + }, + }; + } + })(); + } + + function replyTo(targetMessage: string, newMessage: string): MessageSpec { + return new (class extends MessageSpec { + public async getContent(room: Room): Promise> { + const ev = await getMessage(room, targetMessage); + + return { + "msgtype": "m.text", + "body": newMessage, + "m.relates_to": { + "m.in_reply_to": { + event_id: ev.getId(), + }, + }, + }; + } + })(); + } + + function threadedOff(rootMessage: string, newMessage: string): MessageSpec { + return new (class extends MessageSpec { + public async getContent(room: Room): Promise> { + const ev = await getMessage(room, rootMessage); + + return { + "msgtype": "m.text", + "body": newMessage, + "m.relates_to": { + event_id: ev.getId(), + is_falling_back: true, + rel_type: "m.thread", + }, + }; + } + })(); + } + + function getRoomListTile(room: string) { + return cy.findByRole("treeitem", { name: new RegExp("^" + room) }); + } + + function markAsRead(room: string) { + getRoomListTile(room).rightclick(); + cy.findByText("Mark as read").click(); + } + + function assertRead(room: string) { + return getRoomListTile(room).within(() => { + cy.get(".mx_NotificationBadge_dot").should("not.exist"); + cy.get(".mx_NotificationBadge_count").should("not.exist"); + }); + } + + function assertUnread(room: string, count: number | ".") { + return getRoomListTile(room).within(() => { + if (count === ".") { + cy.get(".mx_NotificationBadge_dot").should("exist"); + } else { + cy.get(".mx_NotificationBadge_count").should("have.text", count); + } + }); + } + + function openThreadList() { + cy.findByTestId("threadsButton").then((button) => { + if (button?.attr("aria-current") !== "true") { + button.trigger("click"); + } + }); + cy.get(".mx_ThreadPanel").should("exist"); + // If the Threads back button is present then click it, the threads button can open either threads list or thread panel + Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); + } + + function getThreadListTile(rootMessage: string) { + openThreadList(); + return cy.contains(".mx_ThreadPanel .mx_EventTile_body", rootMessage).closest("li"); + } + + function assertReadThread(rootMessage: string) { + return getThreadListTile(rootMessage).within(() => { + cy.get(".mx_NotificationBadge").should("not.exist"); + }); + } + + function assertUnreadThread(rootMessage: string) { + return getThreadListTile(rootMessage).within(() => { + cy.get(".mx_NotificationBadge").should("exist"); + }); + } + + function saveAndReload() { + cy.getClient().then((cli) => { + // @ts-ignore + return (cli.store as IndexedDBStore).reallySave(); + }); + cy.reload(); + // Wait for the app to reload + cy.get(".mx_RoomView").should("exist"); + } + + const room1 = selectedRoomName; + const room2 = otherRoomName; + + describe("new messages", () => { + describe("in the main timeline", () => { + it("Sending a message makes a room unread", () => { + goTo(room1); + assertRead(room2); + + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + }); + it("Reading latest message makes the room read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + }); + it.skip("Reading an older message leaves the room unread", () => {}); + it("Marking a room as read makes it read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + markAsRead(room2); + assertRead(room2); + }); + it("Sending a new message after marking as read makes it unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, ["Msg2"]); + assertUnread(room2, 1); + }); + it("A room with a new message is still unread after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + saveAndReload(); + assertUnread(room2, 1); + }); + it("A room where all messages are read is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + markAsRead(room2); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); + }); + + describe("in threads", () => { + it("Sending a message makes a room unread", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + goTo(room2); + + assertRead(room2); + goTo(room1); + + receiveMessages(room2, [threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); + }); + it("Reading the last threaded message makes the room read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); + goTo(room2); + + openThread("Msg1"); + assertRead(room2); + }); + it("Reading a thread message makes the thread read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2, 2); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does appear unread + assertUnread(room2, 2); + + // Until we open the thread + openThread("Msg1"); + assertReadThread("Msg1"); + assertRead(room2); + }); + it.skip("Reading an older thread message (via permalink) leaves the thread unread", () => {}); + it("Reading only one thread's message does not make the room read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]); + assertUnread(room2, 4); + goTo(room2); + assertUnread(room2, 4); + + openThread("Msg1"); + assertUnread(room2, 1); + }); + // XXX: Fails, but looks like it is working in the UI - needs investigation + it.skip("Reading only one thread's message makes that thread read but not others", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2", threadedOff("Msg1", "Resp1"), threadedOff("Msg2", "Resp2")]); + assertUnread(room2, 4); // (Sanity) + + // When I read the main timeline + goTo(room2); + + assertUnread(room2, 2); + assertUnreadThread("Msg1"); + assertUnreadThread("Msg2"); + + openThread("Msg1"); + assertReadThread("Msg1"); + assertUnreadThread("Msg2"); + }); + it("Reading the main timeline does not mark a thread message as read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2, 3); // (Sanity) + + // When I read the main timeline + goTo(room2); + + assertUnread(room2, 2); + // Then thread does appear unread + assertUnreadThread("Msg1"); + }); + // XXX: fails because the room is still "bold" even though the notification counts all disappear + it.skip("Marking a room with unread threads as read makes it read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2, 3); // (Sanity) + + markAsRead(room2); + assertRead(room2); + }); + // XXX: fails for the same reason as "Marking a room with unread threads as read makes it read" + it.skip("Sending a new thread message after marking as read makes it unread", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + + // When I mark the room as read + markAsRead(room2); + assertRead(room2); + + // Then another message appears in the thread + receiveMessages(room2, [threadedOff("Msg1", "Resp3")]); + + // Then the room becomes unread + assertUnread(room2, 1); + }); + // XXX: fails for the same reason as "Marking a room with unread threads as read makes it read" + it.skip("Sending a new different-thread message after marking as read makes it unread", () => { + // Given 2 threads exist, and Thread2 has the latest message in it + goTo(room1); + receiveMessages(room2, ["Thread1", "Thread2", threadedOff("Thread1", "t1a")]); + assertUnread(room2, 3); + receiveMessages(room2, [threadedOff("Thread2", "t2a")]); + + // When I mark the room as read (making an unthreaded receipt for t2a) + markAsRead(room2); + assertRead(room2); + + // Then another message appears in the other thread + receiveMessages(room2, [threadedOff("Thread1", "t1b")]); + + // Then the room becomes unread + assertUnread(room2, 1); + }); + it("A room with a new threaded message is still unread after restart", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2, 2); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does appear unread + assertUnread(room2, 2); + + saveAndReload(); + assertUnread(room2, 2); + + // Until we open the thread + openThread("Msg1"); + assertRead(room2); + }); + it("A room where all threaded messages are read is still read after restart", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); + assertUnread(room2, 3); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does appear unread + assertUnread(room2, 2); + + // Until we open the thread + openThread("Msg1"); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); + }); + + describe("thread roots", () => { + it("Reading a thread root does not mark the thread as read", () => { + // Given a thread exists + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); // (Sanity) + + // When I read the main timeline + goTo(room2); + + // Then room does appear unread + assertUnread(room2, 2); + assertUnreadThread("Msg1"); + }); + it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); + it("Creating a new thread based on a reply makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); + assertUnread(room2, 3); + }); + it("Reading a thread whose root is a reply makes the room read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); + assertUnread(room2, 3); + + goTo(room2); + assertUnread(room2, 1); + assertUnreadThread("Reply1"); + + openThread("Reply1"); + assertRead(room2); + }); + }); + }); + + describe("editing messages", () => { + describe("in the main timeline", () => { + it("Editing a message makes a room unread", () => { + // Given I am not in the room + goTo(room1); + + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + + // When an edit appears in the room + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + + // Then it becomes unread + assertUnread(room2, 1); + }); + it("Reading an edit makes the room read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + assertUnread(room2, 1); + + // When I read it + goTo(room2); + + // Then the room becomes read and stays read + assertRead(room2); + goTo(room1); + assertRead(room2); + }); + it("Marking a room as read after an edit makes it read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + assertUnread(room2, 1); + + // When I mark it as read + markAsRead(room2); + + // Then the room becomes read + assertRead(room2); + }); + it("Editing a message after marking as read makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + // When I mark it as read + markAsRead(room2); + + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + + // Then the room becomes unread + assertUnread(room2, 1); + }); + it.skip("Editing a reply after reading it makes the room unread", () => {}); + it.skip("Editing a reply after marking as read makes the room unread", () => {}); + it.skip("A room with an edit is still unread after restart", () => {}); + it.skip("A room where all edits are read is still read after restart", () => {}); + }); + + describe("in threads", () => { + it.skip("An edit of a threaded message makes the room unread", () => {}); + it.skip("Reading an edit of a threaded message makes the room read", () => {}); + it.skip("Marking a room as read after an edit in a thread makes it read", () => {}); + it.skip("Editing a thread message after marking as read makes the room unread", () => {}); + it.skip("A room with an edited threaded message is still unread after restart", () => {}); + it.skip("A room where all threaded edits are read is still read after restart", () => {}); + }); + + describe("thread roots", () => { + it.skip("An edit of a thread root makes the room unread", () => {}); + it.skip("Reading an edit of a thread root makes the room read", () => { + // Given a fully-read thread exists + goTo(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + openThread("Msg1"); + goTo(room1); + assertRead(room2); + + // When the thread root is edited + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + + // And I read that edit + goTo(room2); + + // Then the room becomes read and stays read + assertRead(room2); + goTo(room1); + assertRead(room2); + }); + it.skip("Marking a room as read after an edit of a thread root makes it read", () => {}); + it.skip("Editing a thread root after marking as read makes the room unread", () => {}); + it.skip("Marking a room as read after an edit of a thread root that is a reply makes it read", () => {}); + it.skip("Editing a thread root that is a reply after marking as read makes the room unread but not the thread", () => {}); + }); + }); + + 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", () => {}); + }); + + 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.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", () => { + it.skip("A reaction to a thread root makes the room unread", () => {}); + 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", () => {}); + }); + }); + + 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.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", () => {}); + it.skip("Marking an unread room as read after a redaction makes it read", () => {}); + it.skip("Sending and redacting a message after marking the room as read makes it unread", () => {}); + it.skip("?? Redacting a message after marking the room as read makes it unread", () => {}); + it.skip("Reacting to a redacted message leaves the room read", () => {}); + it.skip("Editing a redacted message leaves the room read", () => {}); + + it.skip("?? Reading a reaction to a redacted message marks the room as read", () => {}); + it.skip("?? Reading an edit of a redacted message marks the room as read", () => {}); + it.skip("Reading a reply to a redacted message marks the room as read", () => {}); + + it.skip("A room with an unread redaction is still unread after restart", () => {}); + it.skip("A room with a read redaction is still read after restart", () => {}); + }); + + 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.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", () => {}); + + 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("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.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", () => {}); + }); + + describe("thread roots", () => { + // One of the following two must be right: + it.skip("Redacting a thread root after it was read leaves the room read", () => {}); + it.skip("Redacting a thread root after it was read makes the room unread", () => {}); + + it.skip("Redacting the root of an unread thread makes the room read", () => {}); + it.skip("Sending a threaded message onto a redacted thread root leaves the room read", () => {}); + it.skip("Reacting to a redacted thread root leaves the room read", () => {}); + it.skip("Editing a redacted thread root leaves the room read", () => {}); + it.skip("Replying to a redacted thread root makes the room unread", () => {}); + it.skip("Reading a reply to a redacted thread root makes the room read", () => {}); + }); + }); + + describe("messages with missing referents", () => { + it.skip("A message in an unknown thread is not visible and the room is read", () => {}); + it.skip("When a message's thread root appears later the thread appears and the room is unread", () => {}); + it.skip("An edit of an unknown message is not visible and the room is read", () => {}); + it.skip("When an edit's message appears later the edited version appears and the room is unread", () => {}); + it.skip("A reaction to an unknown message is not visible and the room is read", () => {}); + it.skip("When an reactions's message appears later it appears and the room is unread", () => {}); + // Harder: validate that we request the messages we are missing? + }); + + describe("receipts with missing events", () => { + // Later: when we have order in receipts, we can change these tests to + // make receipts still work, even when their message is not found. + it.skip("A receipt for an unknown message does not change the state of an unread room", () => {}); + it.skip("A receipt for an unknown message does not change the state of a read room", () => {}); + it.skip("A threaded receipt for an unknown message does not change the state of an unread thread", () => {}); + it.skip("A threaded receipt for an unknown message does not change the state of a read thread", () => {}); + it.skip("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {}); + it.skip("A threaded receipt for an unknown thread does not change the state of a read thread", () => {}); + it.skip("A threaded receipt for a message on main does not change the state of an unread room", () => {}); + it.skip("A threaded receipt for a message on main does not change the state of a read room", () => {}); + it.skip("A main receipt for a message on a thread does not change the state of an unread room", () => {}); + it.skip("A main receipt for a message on a thread does not change the state of a read room", () => {}); + it.skip("A threaded receipt for a thread root does not mark it as read", () => {}); + // Harder: validate that we request the messages we are missing? + }); + + describe("Message ordering", () => { + describe("in the main timeline", () => { + it.skip("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {}); + it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {}); + }); + + describe("in threads", () => { + // These don't pass yet - we need MSC4033 - we don't even know the Sync order yet + it.skip("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {}); + it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {}); + + // These pass now and should not later - we should use order from MSC4033 instead of ts + // These are broken out + it.skip("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {}); + it.skip("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {}); + it.skip("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {}); + it.skip("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {}); + it.skip("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {}); + it.skip("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {}); + }); + + describe("thread roots", () => { + it.skip("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {}); + it.skip("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); + it.skip("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {}); + it.skip("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); + }); + }); + + 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.skip("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); + }); + + describe("Paging up", () => { + it.skip("Paging up through old messages after a room is read leaves the room read", () => {}); + it.skip("Paging up through old messages of an unread room leaves the room unread", () => {}); + it.skip("Paging up to find old threads that were previously read leaves the room read", () => {}); + it.skip("?? Paging up to find old threads that were never read marks the room unread", () => {}); + it.skip("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); + }); + + describe("Room list order", () => { + it.skip("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {}); + }); + + describe("Notifications", () => { + describe("in the main timeline", () => { + it.skip("A new message that mentions me shows a notification", () => {}); + it.skip("Reading a notifying message reduces the notification count in the room list, space and tab", () => {}); + it.skip("Reading the last notifying message removes the notification marker from room list, space and tab", () => {}); + it.skip("Editing a message to mentions me shows a notification", () => {}); + it.skip("Reading the last notifying edited message removes the notification marker", () => {}); + it.skip("Redacting a notifying message removes the notification marker", () => {}); + }); + + describe("in threads", () => { + it.skip("A new threaded message that mentions me shows a notification", () => {}); + it.skip("Reading a notifying threaded message removes the notification count", () => {}); + it.skip("Notification count remains steady when reading threads that contain seen notifications", () => {}); + it.skip("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {}); + it.skip("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {}); + it.skip("Redacting a notifying threaded message removes the notification marker", () => {}); + }); + }); +}); diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index 608a99240f3..0329b0af73b 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -18,10 +18,7 @@ limitations under the License. import type { MatrixClient, MatrixEvent, ISendEventResponse } from "matrix-js-sdk/src/matrix"; import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; -import type { Room } from "matrix-js-sdk/src/matrix"; -import type { IndexedDBStore } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import Chainable = Cypress.Chainable; describe("Read receipts", () => { const userName = "Mae"; @@ -356,730 +353,4 @@ describe("Read receipts", () => { }); }); }); - - abstract class MessageSpec { - public abstract getContent(room: Room): Promise>; - } - - type Message = string | MessageSpec; - - function goTo(room: string) { - cy.viewRoomByName(room); - } - - function findRoomByName(room: string): Chainable { - return cy.getClient().then((cli) => { - return cli.getRooms().find((r) => r.name === room); - }); - } - - function openThread(rootMessage: string) { - cy.get(".mx_RoomView_body").within(() => { - cy.contains(".mx_EventTile[data-scroll-tokens]", rootMessage) - .realHover() - .findByRole("button", { name: "Reply in thread" }) - .click(); - }); - cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); - } - - // Sends messages into given room as a bot - function receiveMessages(room: string, messages: Message[]) { - findRoomByName(room).then(async ({ roomId }) => { - const room = bot.getRoom(roomId); - for (const message of messages) { - if (typeof message === "string") { - await bot.sendTextMessage(roomId, message); - } else { - await bot.sendMessage(roomId, await message.getContent(room)); - } - } - }); - } - - async function getMessage(room: Room, message: string): Promise { - const ev = room.timeline.find((e) => e.getContent().body === message); - if (ev) return ev; - - return new Promise((resolve) => { - room.on("Room.timeline" as any, (ev: MatrixEvent) => { - if (ev.getContent().body === message) { - resolve(ev); - } - }); - }); - } - - function editOf(originalMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { - public async getContent(room: Room): Promise> { - const ev = await getMessage(room, originalMessage); - - const content = ev.getContent(); - return { - "msgtype": content.msgtype, - "body": `* ${newMessage}`, - "m.new_content": { - msgtype: content.msgtype, - body: newMessage, - }, - }; - } - })(); - } - - function replyTo(targetMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { - public async getContent(room: Room): Promise> { - const ev = await getMessage(room, targetMessage); - - return { - "msgtype": "m.text", - "body": newMessage, - "m.relates_to": { - "m.in_reply_to": { - event_id: ev.getId(), - }, - }, - }; - } - })(); - } - - function threadedOff(rootMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { - public async getContent(room: Room): Promise> { - const ev = await getMessage(room, rootMessage); - - return { - "msgtype": "m.text", - "body": newMessage, - "m.relates_to": { - event_id: ev.getId(), - is_falling_back: true, - rel_type: "m.thread", - }, - }; - } - })(); - } - - function getRoomListTile(room: string) { - return cy.findByRole("treeitem", { name: new RegExp("^" + room) }); - } - - function markAsRead(room: string) { - getRoomListTile(room).rightclick(); - cy.findByText("Mark as read").click(); - } - - function assertRead(room: string) { - return getRoomListTile(room).within(() => { - cy.get(".mx_NotificationBadge_dot").should("not.exist"); - cy.get(".mx_NotificationBadge_count").should("not.exist"); - }); - } - - function assertUnread(room: string, count: number | ".") { - return getRoomListTile(room).within(() => { - if (count === ".") { - cy.get(".mx_NotificationBadge_dot").should("exist"); - } else { - cy.get(".mx_NotificationBadge_count").should("have.text", count); - } - }); - } - - function openThreadList() { - cy.findByTestId("threadsButton").then((button) => { - if (button?.attr("aria-current") !== "true") { - button.trigger("click"); - } - }); - cy.get(".mx_ThreadPanel").should("exist"); - // If the Threads back button is present then click it, the threads button can open either threads list or thread panel - Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); - } - - function getThreadListTile(rootMessage: string) { - openThreadList(); - return cy.contains(".mx_ThreadPanel .mx_EventTile_body", rootMessage).closest("li"); - } - - function assertReadThread(rootMessage: string) { - return getThreadListTile(rootMessage).within(() => { - cy.get(".mx_NotificationBadge").should("not.exist"); - }); - } - - function assertUnreadThread(rootMessage: string) { - return getThreadListTile(rootMessage).within(() => { - cy.get(".mx_NotificationBadge").should("exist"); - }); - } - - function saveAndReload() { - cy.getClient().then((cli) => { - // @ts-ignore - return (cli.store as IndexedDBStore).reallySave(); - }); - cy.reload(); - // Wait for the app to reload - cy.get(".mx_RoomView").should("exist"); - } - - const room1 = selectedRoomName; - const room2 = otherRoomName; - - describe("new messages", () => { - describe("in the main timeline", () => { - it("Sending a message makes a room unread", () => { - goTo(room1); - assertRead(room2); - - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - }); - it("Reading latest message makes the room read", () => { - goTo(room1); - assertRead(room2); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - - // When I read the main timeline - goTo(room2); - assertRead(room2); - }); - it.skip("Reading an older message leaves the room unread", () => {}); - it("Marking a room as read makes it read", () => { - goTo(room1); - assertRead(room2); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - - markAsRead(room2); - assertRead(room2); - }); - it("Sending a new message after marking as read makes it unread", () => { - goTo(room1); - assertRead(room2); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - - markAsRead(room2); - assertRead(room2); - - receiveMessages(room2, ["Msg2"]); - assertUnread(room2, 1); - }); - it("A room with a new message is still unread after restart", () => { - goTo(room1); - assertRead(room2); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - - saveAndReload(); - assertUnread(room2, 1); - }); - it("A room where all messages are read is still read after restart", () => { - goTo(room1); - assertRead(room2); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - - markAsRead(room2); - assertRead(room2); - - saveAndReload(); - assertRead(room2); - }); - }); - - describe("in threads", () => { - it("Sending a message makes a room unread", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - goTo(room2); - - assertRead(room2); - goTo(room1); - - receiveMessages(room2, [threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); - }); - it("Reading the last threaded message makes the room read", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); - goTo(room2); - - openThread("Msg1"); - assertRead(room2); - }); - it("Reading a thread message makes the thread read", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) - - // When I read the main timeline - goTo(room2); - - // Then room does appear unread - assertUnread(room2, 2); - - // Until we open the thread - openThread("Msg1"); - assertReadThread("Msg1"); - assertRead(room2); - }); - it.skip("Reading an older thread message (via permalink) leaves the thread unread", () => {}); - it("Reading only one thread's message does not make the room read", () => { - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]); - assertUnread(room2, 4); - goTo(room2); - assertUnread(room2, 4); - - openThread("Msg1"); - assertUnread(room2, 1); - }); - // XXX: Fails, but looks like it is working in the UI - needs investigation - it.skip("Reading only one thread's message makes that thread read but not others", () => { - goTo(room1); - receiveMessages(room2, ["Msg1", "Msg2", threadedOff("Msg1", "Resp1"), threadedOff("Msg2", "Resp2")]); - assertUnread(room2, 4); // (Sanity) - - // When I read the main timeline - goTo(room2); - - assertUnread(room2, 2); - assertUnreadThread("Msg1"); - assertUnreadThread("Msg2"); - - openThread("Msg1"); - assertReadThread("Msg1"); - assertUnreadThread("Msg2"); - }); - it("Reading the main timeline does not mark a thread message as read", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 3); // (Sanity) - - // When I read the main timeline - goTo(room2); - - assertUnread(room2, 2); - // Then thread does appear unread - assertUnreadThread("Msg1"); - }); - // XXX: fails because the room is still "bold" even though the notification counts all disappear - it.skip("Marking a room with unread threads as read makes it read", () => { - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 3); // (Sanity) - - markAsRead(room2); - assertRead(room2); - }); - // XXX: fails for the same reason as "Marking a room with unread threads as read makes it read" - it.skip("Sending a new thread message after marking as read makes it unread", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - - // When I mark the room as read - markAsRead(room2); - assertRead(room2); - - // Then another message appears in the thread - receiveMessages(room2, [threadedOff("Msg1", "Resp3")]); - - // Then the room becomes unread - assertUnread(room2, 1); - }); - // XXX: fails for the same reason as "Marking a room with unread threads as read makes it read" - it.skip("Sending a new different-thread message after marking as read makes it unread", () => { - // Given 2 threads exist, and Thread2 has the latest message in it - goTo(room1); - receiveMessages(room2, ["Thread1", "Thread2", threadedOff("Thread1", "t1a")]); - assertUnread(room2, 3); - receiveMessages(room2, [threadedOff("Thread2", "t2a")]); - - // When I mark the room as read (making an unthreaded receipt for t2a) - markAsRead(room2); - assertRead(room2); - - // Then another message appears in the other thread - receiveMessages(room2, [threadedOff("Thread1", "t1b")]); - - // Then the room becomes unread - assertUnread(room2, 1); - }); - it("A room with a new threaded message is still unread after restart", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) - - // When I read the main timeline - goTo(room2); - - // Then room does appear unread - assertUnread(room2, 2); - - saveAndReload(); - assertUnread(room2, 2); - - // Until we open the thread - openThread("Msg1"); - assertRead(room2); - }); - it("A room where all threaded messages are read is still read after restart", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 3); // (Sanity) - - // When I read the main timeline - goTo(room2); - - // Then room does appear unread - assertUnread(room2, 2); - - // Until we open the thread - openThread("Msg1"); - assertRead(room2); - - saveAndReload(); - assertRead(room2); - }); - }); - - describe("thread roots", () => { - it("Reading a thread root does not mark the thread as read", () => { - // Given a thread exists - goTo(room1); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); // (Sanity) - - // When I read the main timeline - goTo(room2); - - // Then room does appear unread - assertUnread(room2, 2); - assertUnreadThread("Msg1"); - }); - it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); - it("Creating a new thread based on a reply makes the room unread", () => { - goTo(room1); - receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); - assertUnread(room2, 3); - }); - it("Reading a thread whose root is a reply makes the room read", () => { - goTo(room1); - receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); - assertUnread(room2, 3); - - goTo(room2); - assertUnread(room2, 1); - assertUnreadThread("Reply1"); - - openThread("Reply1"); - assertRead(room2); - }); - }); - }); - - describe("editing messages", () => { - describe("in the main timeline", () => { - it("Editing a message makes a room unread", () => { - // Given I am not in the room - goTo(room1); - - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - markAsRead(room2); - - // When an edit appears in the room - receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); - - // Then it becomes unread - assertUnread(room2, 1); - }); - it("Reading an edit makes the room read", () => { - goTo(room1); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - markAsRead(room2); - receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); - assertUnread(room2, 1); - - // When I read it - goTo(room2); - - // Then the room becomes read and stays read - assertRead(room2); - goTo(room1); - assertRead(room2); - }); - it("Marking a room as read after an edit makes it read", () => { - goTo(room1); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - markAsRead(room2); - receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); - assertUnread(room2, 1); - - // When I mark it as read - markAsRead(room2); - - // Then the room becomes read - assertRead(room2); - }); - it("Editing a message after marking as read makes the room unread", () => { - goTo(room1); - receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - - // When I mark it as read - markAsRead(room2); - - receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); - - // Then the room becomes unread - assertUnread(room2, 1); - }); - it.skip("Editing a reply after reading it makes the room unread", () => {}); - it.skip("Editing a reply after marking as read makes the room unread", () => {}); - it.skip("A room with an edit is still unread after restart", () => {}); - it.skip("A room where all edits are read is still read after restart", () => {}); - }); - - describe("in threads", () => { - it.skip("An edit of a threaded message makes the room unread", () => {}); - it.skip("Reading an edit of a threaded message makes the room read", () => {}); - it.skip("Marking a room as read after an edit in a thread makes it read", () => {}); - it.skip("Editing a thread message after marking as read makes the room unread", () => {}); - it.skip("A room with an edited threaded message is still unread after restart", () => {}); - it.skip("A room where all threaded edits are read is still read after restart", () => {}); - }); - - describe("thread roots", () => { - it.skip("An edit of a thread root makes the room unread", () => {}); - it.skip("Reading an edit of a thread root makes the room read", () => { - // Given a fully-read thread exists - goTo(room2); - receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - openThread("Msg1"); - goTo(room1); - assertRead(room2); - - // When the thread root is edited - receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); - - // And I read that edit - goTo(room2); - - // Then the room becomes read and stays read - assertRead(room2); - goTo(room1); - assertRead(room2); - }); - it.skip("Marking a room as read after an edit of a thread root makes it read", () => {}); - it.skip("Editing a thread root after marking as read makes the room unread", () => {}); - it.skip("Marking a room as read after an edit of a thread root that is a reply makes it read", () => {}); - it.skip("Editing a thread root that is a reply after marking as read makes the room unread but not the thread", () => {}); - }); - }); - - 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", () => {}); - }); - - 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.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", () => { - it.skip("A reaction to a thread root makes the room unread", () => {}); - 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", () => {}); - }); - }); - - 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.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", () => {}); - it.skip("Marking an unread room as read after a redaction makes it read", () => {}); - it.skip("Sending and redacting a message after marking the room as read makes it unread", () => {}); - it.skip("?? Redacting a message after marking the room as read makes it unread", () => {}); - it.skip("Reacting to a redacted message leaves the room read", () => {}); - it.skip("Editing a redacted message leaves the room read", () => {}); - - it.skip("?? Reading a reaction to a redacted message marks the room as read", () => {}); - it.skip("?? Reading an edit of a redacted message marks the room as read", () => {}); - it.skip("Reading a reply to a redacted message marks the room as read", () => {}); - - it.skip("A room with an unread redaction is still unread after restart", () => {}); - it.skip("A room with a read redaction is still read after restart", () => {}); - }); - - 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.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", () => {}); - - 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("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.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", () => {}); - }); - - describe("thread roots", () => { - // One of the following two must be right: - it.skip("Redacting a thread root after it was read leaves the room read", () => {}); - it.skip("Redacting a thread root after it was read makes the room unread", () => {}); - - it.skip("Redacting the root of an unread thread makes the room read", () => {}); - it.skip("Sending a threaded message onto a redacted thread root leaves the room read", () => {}); - it.skip("Reacting to a redacted thread root leaves the room read", () => {}); - it.skip("Editing a redacted thread root leaves the room read", () => {}); - it.skip("Replying to a redacted thread root makes the room unread", () => {}); - it.skip("Reading a reply to a redacted thread root makes the room read", () => {}); - }); - }); - - describe("messages with missing referents", () => { - it.skip("A message in an unknown thread is not visible and the room is read", () => {}); - it.skip("When a message's thread root appears later the thread appears and the room is unread", () => {}); - it.skip("An edit of an unknown message is not visible and the room is read", () => {}); - it.skip("When an edit's message appears later the edited version appears and the room is unread", () => {}); - it.skip("A reaction to an unknown message is not visible and the room is read", () => {}); - it.skip("When an reactions's message appears later it appears and the room is unread", () => {}); - // Harder: validate that we request the messages we are missing? - }); - - describe("receipts with missing events", () => { - // Later: when we have order in receipts, we can change these tests to - // make receipts still work, even when their message is not found. - it.skip("A receipt for an unknown message does not change the state of an unread room", () => {}); - it.skip("A receipt for an unknown message does not change the state of a read room", () => {}); - it.skip("A threaded receipt for an unknown message does not change the state of an unread thread", () => {}); - it.skip("A threaded receipt for an unknown message does not change the state of a read thread", () => {}); - it.skip("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {}); - it.skip("A threaded receipt for an unknown thread does not change the state of a read thread", () => {}); - it.skip("A threaded receipt for a message on main does not change the state of an unread room", () => {}); - it.skip("A threaded receipt for a message on main does not change the state of a read room", () => {}); - it.skip("A main receipt for a message on a thread does not change the state of an unread room", () => {}); - it.skip("A main receipt for a message on a thread does not change the state of a read room", () => {}); - it.skip("A threaded receipt for a thread root does not mark it as read", () => {}); - // Harder: validate that we request the messages we are missing? - }); - - describe("Message ordering", () => { - describe("in the main timeline", () => { - it.skip("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {}); - it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {}); - }); - - describe("in threads", () => { - // These don't pass yet - we need MSC4033 - we don't even know the Sync order yet - it.skip("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {}); - it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {}); - - // These pass now and should not later - we should use order from MSC4033 instead of ts - // These are broken out - it.skip("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {}); - it.skip("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {}); - it.skip("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {}); - it.skip("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {}); - it.skip("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {}); - it.skip("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {}); - }); - - describe("thread roots", () => { - it.skip("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {}); - it.skip("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); - it.skip("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {}); - it.skip("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {}); - }); - }); - - 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.skip("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); - }); - - describe("Paging up", () => { - it.skip("Paging up through old messages after a room is read leaves the room read", () => {}); - it.skip("Paging up through old messages of an unread room leaves the room unread", () => {}); - it.skip("Paging up to find old threads that were previously read leaves the room read", () => {}); - it.skip("?? Paging up to find old threads that were never read marks the room unread", () => {}); - it.skip("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {}); - }); - - describe("Room list order", () => { - it.skip("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {}); - }); - - describe("Notifications", () => { - describe("in the main timeline", () => { - it.skip("A new message that mentions me shows a notification", () => {}); - it.skip("Reading a notifying message reduces the notification count in the room list, space and tab", () => {}); - it.skip("Reading the last notifying message removes the notification marker from room list, space and tab", () => {}); - it.skip("Editing a message to mentions me shows a notification", () => {}); - it.skip("Reading the last notifying edited message removes the notification marker", () => {}); - it.skip("Redacting a notifying message removes the notification marker", () => {}); - }); - - describe("in threads", () => { - it.skip("A new threaded message that mentions me shows a notification", () => {}); - it.skip("Reading a notifying threaded message removes the notification count", () => {}); - it.skip("Notification count remains steady when reading threads that contain seen notifications", () => {}); - it.skip("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {}); - it.skip("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {}); - it.skip("Redacting a notifying threaded message removes the notification marker", () => {}); - }); - }); }); From 17be1c67b1059f0a3089e5437d3bcad19de09ae9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Aug 2023 13:27:09 +0100 Subject: [PATCH 28/37] Attempt to fix test --- cypress/e2e/read-receipts/high-level.spec.ts | 46 +++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index f45edf8e575..29a3959fe2e 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -90,13 +90,14 @@ describe("Read receipts", () => { } function openThread(rootMessage: string) { - cy.get(".mx_RoomView_body").within(() => { - cy.contains(".mx_EventTile[data-scroll-tokens]", rootMessage) + cy.log("Open thread", rootMessage); + cy.get(".mx_RoomView_body", { log: false }).within(() => { + cy.contains(".mx_EventTile[data-scroll-tokens]", rootMessage, { log: false }) .realHover() - .findByRole("button", { name: "Reply in thread" }) + .findByRole("button", { name: "Reply in thread", log: false }) .click(); }); - cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); + cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1); } // Sends messages into given room as a bot @@ -181,15 +182,17 @@ describe("Read receipts", () => { } function getRoomListTile(room: string) { - return cy.findByRole("treeitem", { name: new RegExp("^" + room) }); + return cy.findByRole("treeitem", { name: new RegExp("^" + room), log: false }); } function markAsRead(room: string) { + cy.log("Marking room as read", room); getRoomListTile(room).rightclick(); cy.findByText("Mark as read").click(); } function assertRead(room: string) { + cy.log("Assert room read", room); return getRoomListTile(room).within(() => { cy.get(".mx_NotificationBadge_dot").should("not.exist"); cy.get(".mx_NotificationBadge_count").should("not.exist"); @@ -197,6 +200,7 @@ describe("Read receipts", () => { } function assertUnread(room: string, count: number | ".") { + cy.log("Assert room unread", room, count); return getRoomListTile(room).within(() => { if (count === ".") { cy.get(".mx_NotificationBadge_dot").should("exist"); @@ -207,41 +211,52 @@ describe("Read receipts", () => { } function openThreadList() { - cy.findByTestId("threadsButton").then((button) => { - if (button?.attr("aria-current") !== "true") { - button.trigger("click"); + cy.log("Open thread list"); + cy.findByTestId("threadsButton", { log: false }).then(($button) => { + if ($button?.attr("aria-current") !== "true") { + $button.trigger("click"); } }); - cy.get(".mx_ThreadPanel").should("exist"); - // If the Threads back button is present then click it, the threads button can open either threads list or thread panel - Cypress.$('.mx_BaseCard_back[title="Threads"]')?.trigger("click"); + + cy.get(".mx_ThreadPanel", { log: false }) + .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 ($button.length) { + $button.trigger("click"); + } + }); } function getThreadListTile(rootMessage: string) { openThreadList(); - return cy.contains(".mx_ThreadPanel .mx_EventTile_body", rootMessage).closest("li"); + return cy.contains(".mx_ThreadPanel .mx_EventTile_body", rootMessage, { log: false }).closest("li"); } function assertReadThread(rootMessage: string) { return getThreadListTile(rootMessage).within(() => { - cy.get(".mx_NotificationBadge").should("not.exist"); + cy.get(".mx_NotificationBadge", { log: false }).should("not.exist"); }); } function assertUnreadThread(rootMessage: string) { + cy.log("Assert unread thread", rootMessage); return getThreadListTile(rootMessage).within(() => { cy.get(".mx_NotificationBadge").should("exist"); }); } function saveAndReload() { + cy.log("Save and reload"); cy.getClient().then((cli) => { // @ts-ignore return (cli.store as IndexedDBStore).reallySave(); }); cy.reload(); // Wait for the app to reload - cy.get(".mx_RoomView").should("exist"); + cy.log("Waiting for app to reload"); + cy.get(".mx_RoomView", { log: false }).should("exist"); } const room1 = selectedRoomName; @@ -363,8 +378,7 @@ describe("Read receipts", () => { openThread("Msg1"); assertUnread(room2, 1); }); - // XXX: Fails, but looks like it is working in the UI - needs investigation - it.skip("Reading only one thread's message makes that thread read but not others", () => { + it("Reading only one thread's message makes that thread read but not others", () => { goTo(room1); receiveMessages(room2, ["Msg1", "Msg2", threadedOff("Msg1", "Resp1"), threadedOff("Msg2", "Resp2")]); assertUnread(room2, 4); // (Sanity) From 7530eacf91d4be9b2add95604e8e5d830db4b20c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Aug 2023 13:59:00 +0100 Subject: [PATCH 29/37] Wire up more tests --- cypress/e2e/read-receipts/high-level.spec.ts | 77 ++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 29a3959fe2e..54d476f10df 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -545,7 +545,11 @@ describe("Read receipts", () => { goTo(room1); receiveMessages(room2, ["Msg1"]); assertUnread(room2, 1); - markAsRead(room2); + + goTo(room2); + assertRead(room2); + goTo(room1); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); assertUnread(room2, 1); @@ -584,10 +588,73 @@ describe("Read receipts", () => { // Then the room becomes unread assertUnread(room2, 1); }); - it.skip("Editing a reply after reading it makes the room unread", () => {}); - it.skip("Editing a reply after marking as read makes the room unread", () => {}); - it.skip("A room with an edit is still unread after restart", () => {}); - it.skip("A room where all edits are read is still read after restart", () => {}); + it("Editing a reply after reading it makes the room unread", () => { + // Given I am not in the room + goTo(room1); + + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); + assertUnread(room2, 1); + + goTo(room2); + assertRead(room2); + goTo(room1); + + // When an edit appears in the room + receiveMessages(room2, [editOf("Reply1", "Reply1 Edit1")]); + + // Then it becomes unread + assertUnread(room2, 1); + }); + it("Editing a reply after marking as read makes the room unread", () => { + // Given I am not in the room + goTo(room1); + + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); + assertUnread(room2, 1); + markAsRead(room2); + + // When an edit appears in the room + receiveMessages(room2, [editOf("Reply1", "Reply1 Edit1")]); + + // Then it becomes unread + assertUnread(room2, 1); + }); + it("A room with an edit is still unread after restart", () => { + // Given I am not in the room + goTo(room1); + + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + + // When an edit appears in the room + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + + // Then it becomes unread + assertUnread(room2, 1); + + // And remains so after a reload + saveAndReload(); + assertUnread(room2, 1); + }); + it("A room where all edits are read is still read after restart", () => { + goTo(room1); + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + markAsRead(room2); + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + assertUnread(room2, 1); + + // When I mark it as read + markAsRead(room2); + + // Then the room becomes read + assertRead(room2); + + // And remains so after a reload + saveAndReload(); + assertRead(room2); + }); }); describe("in threads", () => { From d731af1e1573768b16e5daf4d8b23e89e4011ce2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Aug 2023 14:28:44 +0100 Subject: [PATCH 30/37] Wire up more tests --- cypress/e2e/read-receipts/high-level.spec.ts | 45 +++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 54d476f10df..04c23b699dd 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -114,8 +114,15 @@ describe("Read receipts", () => { }); } - async function getMessage(room: Room, message: string): Promise { - const ev = room.timeline.find((e) => e.getContent().body === message); + async function getMessage(room: Room, message: string, includeThreads = false): Promise { + let ev = room.timeline.find((e) => e.getContent().body === message); + if (!ev && includeThreads) { + for (const thread of room.getThreads()) { + ev = thread.timeline.find((e) => e.getContent().body === message); + if (ev) break; + } + } + if (ev) return ev; return new Promise((resolve) => { @@ -130,7 +137,7 @@ describe("Read receipts", () => { function editOf(originalMessage: string, newMessage: string): MessageSpec { return new (class extends MessageSpec { public async getContent(room: Room): Promise> { - const ev = await getMessage(room, originalMessage); + const ev = await getMessage(room, originalMessage, true); const content = ev.getContent(); return { @@ -658,8 +665,36 @@ describe("Read receipts", () => { }); describe("in threads", () => { - it.skip("An edit of a threaded message makes the room unread", () => {}); - it.skip("Reading an edit of a threaded message makes the room read", () => {}); + it("An edit of a threaded message makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + goTo(room1); + + receiveMessages(room2, [editOf("Resp1", "Edit1")]); + assertUnread(room2, 1); + }); + it("Reading an edit of a threaded message makes the room read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + goTo(room1); + + receiveMessages(room2, [editOf("Resp1", "Edit1")]); + assertUnread(room2, 1); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + }); it.skip("Marking a room as read after an edit in a thread makes it read", () => {}); it.skip("Editing a thread message after marking as read makes the room unread", () => {}); it.skip("A room with an edited threaded message is still unread after restart", () => {}); From d7f75a96ab933ccf616b2f9166e328e9db4375f4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Aug 2023 10:51:58 +0100 Subject: [PATCH 31/37] Wire up more tests --- cypress/e2e/read-receipts/high-level.spec.ts | 44 +++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 04c23b699dd..7fba995b9f3 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -263,7 +263,7 @@ describe("Read receipts", () => { cy.reload(); // Wait for the app to reload cy.log("Waiting for app to reload"); - cy.get(".mx_RoomView", { log: false }).should("exist"); + cy.get(".mx_RoomView", { log: false, timeout: 20000 }).should("exist"); } const room1 = selectedRoomName; @@ -695,10 +695,44 @@ describe("Read receipts", () => { openThread("Msg1"); assertRead(room2); }); - it.skip("Marking a room as read after an edit in a thread makes it read", () => {}); - it.skip("Editing a thread message after marking as read makes the room unread", () => {}); - it.skip("A room with an edited threaded message is still unread after restart", () => {}); - it.skip("A room where all threaded edits are read is still read after restart", () => {}); + it("Marking a room as read after an edit in a thread makes it read", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + }); + it.skip("Editing a thread message after marking as read makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [editOf("Resp1", "Edit1")]); + assertUnread(room2, 1); + }); + it("A room with an edited threaded message is still unread after restart", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); + assertUnread(room2, 3); + + saveAndReload(); + assertUnread(room2, 3); + }); + it("A room where all threaded edits are read is still read after restart", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); + assertUnread(room2, 3); + + markAsRead(room2); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); }); describe("thread roots", () => { From 9b62df110dc143f1191a027898ad643955719ac4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Aug 2023 10:54:20 +0100 Subject: [PATCH 32/37] Wire up more tests --- cypress/e2e/read-receipts/high-level.spec.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 7fba995b9f3..39aea2c18fb 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -736,7 +736,19 @@ describe("Read receipts", () => { }); describe("thread roots", () => { - it.skip("An edit of a thread root makes the room unread", () => {}); + it("An edit of a thread root makes the room unread", () => { + goTo(room1); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); + assertUnread(room2, 1); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + goTo(room1); + + receiveMessages(room2, [editOf("Msg1", "Edit1")]); + assertUnread(room2, 1); + }); it.skip("Reading an edit of a thread root makes the room read", () => { // Given a fully-read thread exists goTo(room2); From 4899cfee9bdd819b9b638c9109a2468a756ea17c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Aug 2023 10:43:10 +0100 Subject: [PATCH 33/37] Iterate --- cypress/e2e/read-receipts/high-level.spec.ts | 49 +++++++++++--------- cypress/support/views.ts | 2 + 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 39aea2c18fb..5c7898c2910 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -23,12 +23,12 @@ import Chainable = Cypress.Chainable; describe("Read receipts", () => { const userName = "Mae"; const botName = "Other User"; - const selectedRoomName = "Selected Room"; - const otherRoomName = "Other Room"; + const roomAlpha = "Room Alpha"; + const roomBeta = "Room Beta"; let homeserver: HomeserverInstance; - let otherRoomId: string; - let selectedRoomId: string; + let betaRoomId: string; + let alphaRoomId: string; let bot: MatrixClient | undefined; beforeEach(() => { @@ -43,13 +43,13 @@ describe("Read receipts", () => { homeserver = data; cy.initTestUser(homeserver, userName) .then(() => { - cy.createRoom({ name: selectedRoomName }).then((createdRoomId) => { - selectedRoomId = createdRoomId; + cy.createRoom({ name: roomAlpha }).then((createdRoomId) => { + alphaRoomId = createdRoomId; }); }) .then(() => { - cy.createRoom({ name: otherRoomName }).then((createdRoomId) => { - otherRoomId = createdRoomId; + cy.createRoom({ name: roomBeta }).then((createdRoomId) => { + betaRoomId = createdRoomId; }); }) .then(() => { @@ -58,13 +58,9 @@ describe("Read receipts", () => { }); }) .then(() => { - // Invite the bot to Other room - cy.inviteUser(otherRoomId, bot.getUserId()); - cy.visit("/#/room/" + otherRoomId); - cy.findByText(botName + " joined the room").should("exist"); - - // Then go into Selected room - cy.visit("/#/room/" + selectedRoomId); + // Invite the bot to both rooms + cy.inviteUser(alphaRoomId, bot.getUserId()); + cy.inviteUser(betaRoomId, bot.getUserId()); }); }); }); @@ -100,7 +96,11 @@ describe("Read receipts", () => { cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1); } - // Sends messages into given room as a bot + /** + * 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[]) { findRoomByName(room).then(async ({ roomId }) => { const room = bot.getRoom(roomId); @@ -206,6 +206,11 @@ describe("Read receipts", () => { }); } + /** + * Assert a given room is marked as unread (via the room list tile) + * @param room - the name of the room to check + * @param count - the numeric count to assert, or if "." specified then a bold/dot (no count) state is asserted + */ function assertUnread(room: string, count: number | ".") { cy.log("Assert room unread", room, count); return getRoomListTile(room).within(() => { @@ -266,8 +271,8 @@ describe("Read receipts", () => { cy.get(".mx_RoomView", { log: false, timeout: 20000 }).should("exist"); } - const room1 = selectedRoomName; - const room2 = otherRoomName; + const room1 = roomAlpha; + const room2 = roomBeta; describe("new messages", () => { describe("in the main timeline", () => { @@ -535,7 +540,7 @@ describe("Read receipts", () => { describe("editing messages", () => { describe("in the main timeline", () => { it("Editing a message makes a room unread", () => { - // Given I am not in the room + // Given I am not looking at the room goTo(room1); receiveMessages(room2, ["Msg1"]); @@ -596,7 +601,7 @@ describe("Read receipts", () => { assertUnread(room2, 1); }); it("Editing a reply after reading it makes the room unread", () => { - // Given I am not in the room + // Given I am not looking at the room goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); @@ -613,7 +618,7 @@ describe("Read receipts", () => { assertUnread(room2, 1); }); it("Editing a reply after marking as read makes the room unread", () => { - // Given I am not in the room + // Given I am not looking at the room goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); @@ -627,7 +632,7 @@ describe("Read receipts", () => { assertUnread(room2, 1); }); it("A room with an edit is still unread after restart", () => { - // Given I am not in the room + // Given I am not looking at the room goTo(room1); receiveMessages(room2, ["Msg1"]); diff --git a/cypress/support/views.ts b/cypress/support/views.ts index a5936279bad..b6f817469e2 100644 --- a/cypress/support/views.ts +++ b/cypress/support/views.ts @@ -63,6 +63,8 @@ declare global { } Cypress.Commands.add("viewRoomByName", (name: string): Chainable> => { + // We use a regexp here to search for starts with given name as room tiles have notification labels appended onto them + // e.g. "Room name 3 unread messages." return cy .findByRole("treeitem", { name: new RegExp("^" + name) }) .should("have.class", "mx_RoomTile") From da95fbb36d1420306d1ca4f2cda1fb85610a3ef0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Aug 2023 10:46:26 +0100 Subject: [PATCH 34/37] Add comments --- cypress/e2e/read-receipts/high-level.spec.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 5c7898c2910..24d51fbcbd7 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -114,6 +114,12 @@ describe("Read receipts", () => { }); } + /** + * Utility to find a MatrixEvent by its body content + * @param room - the room to search for the event in + * @param message - the body of the event to search for + * @param includeThreads - whether to search within threads too + */ async function getMessage(room: Room, message: string, includeThreads = false): Promise { let ev = room.timeline.find((e) => e.getContent().body === message); if (!ev && includeThreads) { @@ -134,6 +140,11 @@ describe("Read receipts", () => { }); } + /** + * MessageSpec 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 { public async getContent(room: Room): Promise> { @@ -152,6 +163,11 @@ describe("Read receipts", () => { })(); } + /** + * MessageSpec 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 { public async getContent(room: Room): Promise> { @@ -170,6 +186,11 @@ describe("Read receipts", () => { })(); } + /** + * MessageSpec 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 { public async getContent(room: Room): Promise> { From cfac7f90eb65a0640935f51815791b0909ddc79e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Aug 2023 11:30:02 +0100 Subject: [PATCH 35/37] Iterate --- cypress/e2e/read-receipts/high-level.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 24d51fbcbd7..f0ccdee01bb 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -60,7 +60,12 @@ describe("Read receipts", () => { .then(() => { // Invite the bot to both rooms cy.inviteUser(alphaRoomId, bot.getUserId()); + cy.viewRoomById(alphaRoomId); + cy.findByText(botName + " joined the room").should("exist"); + cy.inviteUser(betaRoomId, bot.getUserId()); + cy.viewRoomById(betaRoomId); + cy.findByText(botName + " joined the room").should("exist"); }); }); }); From 28a63219342f2acea26ad91f843825a6ef4c3ac4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Aug 2023 16:12:56 +0100 Subject: [PATCH 36/37] Fix comments --- cypress/e2e/read-receipts/high-level.spec.ts | 8 +------- cypress/support/views.ts | 8 ++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index f0ccdee01bb..3d76100f6b7 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -32,13 +32,7 @@ describe("Read receipts", () => { let bot: MatrixClient | undefined; beforeEach(() => { - /* - * Create 2 rooms: - * - * - Selected room - this one is clicked in the UI - * - Other room - this one contains the bot, which will send events so - * we can check its unread state. - */ + // Create 2 rooms: Alpha & Beta, we join the bot to both of them cy.startHomeserver("default").then((data) => { homeserver = data; cy.initTestUser(homeserver, userName) diff --git a/cypress/support/views.ts b/cypress/support/views.ts index b6f817469e2..f31c2d7bf2a 100644 --- a/cypress/support/views.ts +++ b/cypress/support/views.ts @@ -23,8 +23,10 @@ declare global { namespace Cypress { interface Chainable { /** - * Opens the given room by name. The room must be visible in the - * room list. + * Opens the given room by name. The room must be visible in the room list. + * It uses a start-anchored regexp to accommodate for room tiles for unread rooms containing additional + * context in their aria labels, e.g. "Room name 3 unread messages." + * * @param name The room name to find and click on/open. */ viewRoomByName(name: string): Chainable>; @@ -63,8 +65,6 @@ declare global { } Cypress.Commands.add("viewRoomByName", (name: string): Chainable> => { - // We use a regexp here to search for starts with given name as room tiles have notification labels appended onto them - // e.g. "Room name 3 unread messages." return cy .findByRole("treeitem", { name: new RegExp("^" + name) }) .should("have.class", "mx_RoomTile") From d19ad75fb7a245157eb3bf2b8224cde9c852b3e0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 10 Aug 2023 11:11:54 +0100 Subject: [PATCH 37/37] Update cypress/e2e/read-receipts/high-level.spec.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- cypress/e2e/read-receipts/high-level.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 3d76100f6b7..7af0daeb9e4 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -32,7 +32,7 @@ describe("Read receipts", () => { let bot: MatrixClient | undefined; beforeEach(() => { - // Create 2 rooms: Alpha & Beta, we join the bot to both of them + // Create 2 rooms: Alpha & Beta. We join the bot to both of them cy.startHomeserver("default").then((data) => { homeserver = data; cy.initTestUser(homeserver, userName)