diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 76da807a4c..bcce3b3d44 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -18,16 +18,29 @@ limitations under the License. /// -import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { + assertMessageLoaded, + assertMessageNotLoaded, assertRead, + assertReadThread, assertStillRead, assertUnread, + assertUnreadGreaterThan, + assertUnreadThread, + closeThreadsPanel, customEvent, goTo, + many, markAsRead, Message, + MessageContentSpec, + MessageFinder, + openThread, + openThreadList, + pageUp, + saveAndReload, sendMessageAsClient, } from "./read-receipts-utils"; @@ -42,12 +55,19 @@ describe("Read receipts", () => { let alphaRoomId: string; let bot: MatrixClient | undefined; - /** - * Map of message content -> event. Allows us to find e.g. edited or - * redacted messages even if their content has changed or disappeared from - * screen. - */ - const messages = new Map(); + let messageFinder: MessageFinder; + + function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec { + return messageFinder.threadedOff(rootMessage, newMessage); + } + + function manyThreadedOff(rootMessage: string, newMessages: Array): Array { + return messageFinder.manyThreadedOff(rootMessage, newMessages); + } + + function jumpTo(room: string, message: string, includeThreads = false) { + return messageFinder.jumpTo(room, message, includeThreads); + } before(() => { // Note: unusually for the Cypress tests in this repo, we share a single @@ -68,7 +88,7 @@ describe("Read receipts", () => { }); beforeEach(() => { - messages.clear(); + messageFinder = new MessageFinder(); // Create 2 rooms: Alpha & Beta. We join the bot to both of them cy.initTestUser(homeserver, userName) @@ -183,11 +203,219 @@ describe("Read receipts", () => { }); 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", () => {}); + it("Paging up through old messages after a room is read leaves the room read", () => { + // Given lots of messages are in the room, but we have read them + goTo(room1); + receiveMessages(room2, many("Msg", 110)); + assertUnread(room2, 110); + goTo(room2); + assertRead(room2); + goTo(room1); + + // When we restart, so only recent messages are loaded + saveAndReload(); + goTo(room2); + assertMessageNotLoaded("Msg0010"); + + // And we page up, loading in old messages + pageUp(); + cy.wait(200); + pageUp(); + cy.wait(200); + pageUp(); + assertMessageLoaded("Msg0010"); + + // Then the room remains read + assertStillRead(room2); + }); + it("Paging up through old messages of an unread room leaves the room unread", () => { + // Given lots of messages are in the room, and they are not read + goTo(room1); + receiveMessages(room2, many("x\ny\nz\nMsg", 40)); // newline to spread out messages + assertUnread(room2, 40); + + // When I jump to a message in the middle and page up + jumpTo(room2, "x\ny\nz\nMsg0020"); + pageUp(); + + // Then the room is still unread + assertUnreadGreaterThan(room2, 1); + }); + it("Paging up to find old threads that were previously read leaves the room read", () => { + // Given lots of messages in threads are all read + goTo(room1); + receiveMessages(room2, [ + "Root1", + "Root2", + "Root3", + ...manyThreadedOff("Root1", many("T", 20)), + ...manyThreadedOff("Root2", many("T", 20)), + ...manyThreadedOff("Root3", many("T", 20)), + ]); + goTo(room2); + assertUnread(room2, 60); + openThread("Root1"); + assertUnread(room2, 40); + assertReadThread("Root1"); + openThread("Root2"); + assertUnread(room2, 20); + assertReadThread("Root2"); + openThread("Root3"); + assertRead(room2); + assertReadThread("Root3"); + + // When I restart and page up to load old thread roots + goTo(room1); + saveAndReload(); + goTo(room2); + pageUp(); + + // Then the room and threads remain read + assertRead(room2); + assertReadThread("Root1"); + assertReadThread("Root2"); + assertReadThread("Root3"); + }); + it("Paging up to find old threads that were never read keeps the room unread", () => { + // Given lots of messages in threads that are unread + goTo(room1); + receiveMessages(room2, [ + "Root1", + "Root2", + "Root3", + ...manyThreadedOff("Root1", many("T", 2)), + ...manyThreadedOff("Root2", many("T", 2)), + ...manyThreadedOff("Root3", many("T", 2)), + ...many("Msg", 100), + ]); + goTo(room2); + assertUnread(room2, 6); + assertUnreadThread("Root1"); + assertUnreadThread("Root2"); + assertUnreadThread("Root3"); + + // When I restart + closeThreadsPanel(); + goTo(room1); + saveAndReload(); + + // Then the room remembers it's unread + // TODO: I (andyb) think this will fall in an encrypted room + assertUnread(room2, 6); + + // And when I page up to load old thread roots + goTo(room2); + pageUp(); + + // Then the room remains unread + assertUnread(room2, 6); + assertUnreadThread("Root1"); + assertUnreadThread("Root2"); + assertUnreadThread("Root3"); + }); + it("Looking in thread view to find old threads that were never read makes the room unread", () => { + // Given lots of messages in threads that are unread + goTo(room1); + receiveMessages(room2, [ + "Root1", + "Root2", + "Root3", + ...manyThreadedOff("Root1", many("T", 2)), + ...manyThreadedOff("Root2", many("T", 2)), + ...manyThreadedOff("Root3", many("T", 2)), + ...many("Msg", 100), + ]); + goTo(room2); + assertUnread(room2, 6); + assertUnreadThread("Root1"); + assertUnreadThread("Root2"); + assertUnreadThread("Root3"); + + // When I restart + closeThreadsPanel(); + goTo(room1); + saveAndReload(); + + // Then the room remembers it's unread + // TODO: I (andyb) think this will fall in an encrypted room + assertUnread(room2, 6); + + // And when I open the threads view + goTo(room2); + openThreadList(); + + // Then the room remains unread + assertUnread(room2, 6); + assertUnreadThread("Root1"); + assertUnreadThread("Root2"); + assertUnreadThread("Root3"); + }); + it("After marking room as read, paging up to find old threads that were never read leaves the room read", () => { + // Given lots of messages in threads that are unread but I marked as read on a main timeline message + goTo(room1); + receiveMessages(room2, [ + "Root1", + "Root2", + "Root3", + ...manyThreadedOff("Root1", many("T", 2)), + ...manyThreadedOff("Root2", many("T", 2)), + ...manyThreadedOff("Root3", many("T", 2)), + ...many("Msg", 100), + ]); + markAsRead(room2); + assertRead(room2); + + // When I restart + saveAndReload(); + + // Then the room remembers it's read + assertRead(room2); + + // And when I page up to load old thread roots + goTo(room2); + pageUp(); + pageUp(); + pageUp(); + + // Then the room remains read + assertStillRead(room2); + assertReadThread("Root1"); + assertReadThread("Root2"); + assertReadThread("Root3"); + }); + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree + it.skip("After marking room as read based on a thread message, opening threads view to find old threads that were never read leaves the room read", () => { + // Given lots of messages in threads that are unread but I marked as read on a thread message + goTo(room1); + receiveMessages(room2, [ + "Root1", + "Root2", + "Root3", + ...manyThreadedOff("Root1", many("T1-", 2)), + ...manyThreadedOff("Root2", many("T2-", 2)), + ...manyThreadedOff("Root3", many("T3-", 2)), + ...many("Msg", 100), + threadedOff("Msg0099", "Thread off 99"), + ]); + markAsRead(room2); + assertRead(room2); + + // When I restart + saveAndReload(); + + // Then the room remembers it's read + assertRead(room2); + + // And when I page up to load old thread roots + goTo(room2); + openThreadList(); + + // Then the room remains read + assertStillRead(room2); + assertReadThread("Root1"); + assertReadThread("Root2"); + assertReadThread("Root3"); + }); }); describe("Room list order", () => { diff --git a/cypress/e2e/read-receipts/new-messages.spec.ts b/cypress/e2e/read-receipts/new-messages.spec.ts index 8cb3cdb140..66d9f75bef 100644 --- a/cypress/e2e/read-receipts/new-messages.spec.ts +++ b/cypress/e2e/read-receipts/new-messages.spec.ts @@ -176,7 +176,7 @@ describe("Read receipts", () => { assertUnread(room2, 30); // When I jump to one of the older messages - jumpTo(room2, "Msg1"); + jumpTo(room2, "Msg0001"); // Then the room is still unread, but some messages were read assertUnreadLessThan(room2, 30); @@ -321,7 +321,7 @@ describe("Read receipts", () => { assertUnread(room2, 21); // When I read an older message in the thread - jumpTo(room2, "InThread1", true); + jumpTo(room2, "InThread0001", true); assertUnreadLessThan(room2, 21); // TODO: for some reason, we can't find the first message // "InThread0", so I am using the second here. Also, they appear @@ -488,7 +488,7 @@ describe("Read receipts", () => { assertUnread(room2, 62); // Sanity // When I jump to an old message and read the thread - jumpTo(room2, "beforeThread0"); + jumpTo(room2, "beforeThread0000"); openThread("ThreadRoot"); // Then the thread root is marked as read in the main timeline, diff --git a/cypress/e2e/read-receipts/read-receipts-utils.ts b/cypress/e2e/read-receipts/read-receipts-utils.ts index 513fd75348..db181cf171 100644 --- a/cypress/e2e/read-receipts/read-receipts-utils.ts +++ b/cypress/e2e/read-receipts/read-receipts-utils.ts @@ -372,7 +372,7 @@ export function pageUp() { * @param howMany the number of strings to generate */ export function many(prefix: string, howMany: number): Array { - return Array.from(Array(howMany).keys()).map((i) => prefix + i.toFixed()); + return Array.from(Array(howMany).keys()).map((i) => prefix + i.toString().padStart(4, "0")); } /**