mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/

mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts

And a couple of gitignore tweaks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-24 15:43:58 +00:00
parent e7509c92a1
commit 91a3cb03c1
3408 changed files with 28 additions and 32 deletions
@@ -0,0 +1,186 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("editing messages", () => {
test.describe("in threads", () => {
test("An edit of a threaded message makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given we have read the thread
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.assertReadThread("Resp1");
await util.goTo(room1);
// When a message inside it is edited
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
// Then the room and thread are read
await util.assertStillRead(room2);
await util.goTo(room2);
await util.assertReadThread("Msg1");
});
test("Reading an edit of a threaded message makes the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an edited thread message appears after we read it
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.assertReadThread("Resp1");
await util.goTo(room1);
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
await util.assertStillRead(room2);
// When I read it
await util.goTo(room2);
await util.openThread("Msg1");
// Then the room and thread are still read
await util.assertStillRead(room2);
await util.assertReadThread("Msg1");
});
test("Marking a room as read after an edit in a thread makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an edit in a thread is making the room unread
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.editOf("Resp1", "Edit1"),
]);
await util.assertUnread(room2, 1);
// When I mark the room as read
await util.markAsRead(room2);
// Then it is read
await util.assertRead(room2);
await util.assertReadThread("Msg1");
});
test("Editing a thread message after marking as read leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a room is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When a message is edited
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
// Then the room remains read
await util.assertStillRead(room2);
await util.assertReadThread("Msg1");
});
test("A room with an edited threaded message is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an edit in a thread is leaving a room read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.markAsRead(room2);
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
await util.assertStillRead(room2);
// When I restart
await util.saveAndReload();
// Then is it still read
await util.assertRead(room2);
});
test("A room where all threaded edits are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.assertReadThread("Msg1");
await util.goTo(room1); // Make sure we are looking at room1 after reload
await util.assertStillRead(room2);
await util.saveAndReload();
await util.assertRead(room2);
await util.assertReadThread("Msg1");
});
test("A room where all threaded edits are marked as read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.editOf("Resp1", "Edit1"),
]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
await util.assertReadThread("Msg1");
// When I restart
await util.saveAndReload();
// It is still read
await util.assertRead(room2);
await util.assertReadThread("Msg1");
});
});
});
});
@@ -0,0 +1,175 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("editing messages", () => {
test.describe("in the main timeline", () => {
test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
// Given I am not looking at the room
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When an edit appears in the room
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
// Then it remains read
await util.assertStillRead(room2);
});
test("Reading an edit leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
// Given an edit is making the room unread
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
await util.assertStillRead(room2);
// When I read it
await util.goTo(room2);
// Then the room stays read
await util.assertStillRead(room2);
await util.goTo(room1);
await util.assertStillRead(room2);
});
test("Editing a message after marking as read leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given the room is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When a message is edited
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
// Then the room remains read
await util.assertStillRead(room2);
});
test("Editing a reply after reading it makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given the room is all read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When a message is edited
await util.receiveMessages(room2, [msg.editOf("Reply1", "Reply1 Edit1")]);
// Then it remains read
await util.assertStillRead(room2);
});
test("Editing a reply after marking as read makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a reply is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
// When the reply is edited
await util.receiveMessages(room2, [msg.editOf("Reply1", "Reply1 Edit1")]);
// Then the room remains read
await util.assertStillRead(room2);
});
test("A room with an edit is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When an edit appears in the room
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
// Then it remains read
await util.assertStillRead(room2);
// And remains so after a reload
await util.saveAndReload();
await util.assertStillRead(room2);
});
test("An edited message becomes read if it happens while I am looking", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message is marked as read
await util.goTo(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertRead(room2);
// When I see an edit appear in the room I am looking at
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
// Then it becomes read
await util.assertStillRead(room2);
});
test("A room where all edits are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message was edited and read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.editOf("Msg1", "Msg1 Edit1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
// When I reload
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
});
});
});
});
@@ -0,0 +1,174 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("editing messages", () => {
test.describe("thread roots", () => {
test("An edit of a thread root leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have read a thread
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.backToThreadsList();
await util.assertRead(room2);
await util.goTo(room1);
// When the thread root is edited
await util.receiveMessages(room2, [msg.editOf("Msg1", "Edit1")]);
// Then the room is read
await util.assertStillRead(room2);
// And the thread is read
await util.goTo(room2);
await util.assertStillRead(room2);
await util.assertReadThread("Edit1");
});
test("Reading an edit of a thread root leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a fully-read thread exists
await util.goTo(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1);
await util.assertRead(room2);
// When the thread root is edited
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
// And I read that edit
await util.goTo(room2);
// Then the room becomes read and stays read
await util.assertStillRead(room2);
await util.goTo(room1);
await util.assertStillRead(room2);
});
test("Editing a thread root after reading leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a fully-read thread exists
await util.goTo(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1);
// When the thread root is edited
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]);
// Then the room stays read
await util.assertStillRead(room2);
});
test("Marking a room as read after an edit of a thread root keeps it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a fully-read thread exists
await util.goTo(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1);
await util.assertRead(room2);
// When the thread root is edited (and I receive another message
// to allow Mark as read)
await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1"), "Msg2"]);
// And when I mark the room as read
await util.markAsRead(room2);
// Then the room becomes read and stays read
await util.assertStillRead(room2);
await util.goTo(room1);
await util.assertStillRead(room2);
});
test("Editing a thread root that is a reply after marking as read leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread based on a reply exists and is read because it is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg",
msg.replyTo("Msg", "Reply"),
msg.threadedOff("Reply", "InThread"),
]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I edit the thread root
await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]);
// Then the room is read
await util.assertStillRead(room2);
// And the thread is read
await util.goTo(room2);
await util.assertReadThread("Edited Reply");
});
test("Marking a room as read after an edit of a thread root that is a reply leaves it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread based on a reply exists and the reply has been edited
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg",
msg.replyTo("Msg", "Reply"),
msg.threadedOff("Reply", "InThread"),
]);
await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]);
await util.assertUnread(room2, 2);
// When I mark the room as read
await util.markAsRead(room2);
// Then the room and thread are read
await util.assertStillRead(room2);
await util.goTo(room2);
await util.assertReadThread("Edited Reply");
});
});
});
});
@@ -0,0 +1,342 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { customEvent, many, test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.slow();
test.describe("Ignored events", () => {
test("If all events after receipt are unimportant, the room is read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
}) => {
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
await util.receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]);
await util.assertRead(room2);
});
test("Sending an important event after unimportant ones makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
}) => {
// Given We have read the important messages
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When we receive unimportant messages
await util.receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]);
// Then the room is still read
await util.assertStillRead(room2);
// And when we receive more important ones
await util.receiveMessages(room2, ["Hello"]);
// The room is unread again
await util.assertUnread(room2, 1);
});
test("A receipt for the last unimportant event makes the room read, even if all are unimportant", async ({
roomAlpha: room1,
roomBeta: room2,
util,
}) => {
// Display room 1
await util.goTo(room1);
// The room 2 is read
await util.assertRead(room2);
// We received 3 unimportant messages to room2
await util.receiveMessages(room2, [
customEvent("org.custom.event", { body: "foobar1" }),
customEvent("org.custom.event", { body: "foobar2" }),
customEvent("org.custom.event", { body: "foobar3" }),
]);
// The room 2 is still read
await util.assertStillRead(room2);
});
});
test.describe("Paging up", () => {
test("Paging up through old messages after a room is read leaves the room read", async ({
page,
roomAlpha: room1,
roomBeta: room2,
util,
}) => {
// Given lots of messages are in the room, but we have read them
await util.goTo(room1);
await util.receiveMessages(room2, many("Msg", 110));
await util.assertUnread(room2, 110);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When we restart, so only recent messages are loaded
await util.saveAndReload();
await util.goTo(room2);
await util.assertMessageNotLoaded("Msg0010");
// And we page up, loading in old messages
await util.pageUp();
await page.waitForTimeout(200);
await util.pageUp();
await page.waitForTimeout(200);
await util.pageUp();
await util.assertMessageLoaded("Msg0010");
// Then the room remains read
await util.assertStillRead(room2);
});
test("Paging up through old messages of an unread room leaves the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given lots of messages are in the room, and they are not read
await util.goTo(room1);
await util.receiveMessages(room2, many("x\ny\nz\nMsg", 40)); // newline to spread out messages
await util.assertUnread(room2, 40);
// When I jump to a message in the middle and page up
await msg.jumpTo(room2, "x\ny\nz\nMsg0020");
await util.pageUp();
// Then the room is still unread
await util.assertUnreadGreaterThan(room2, 1);
});
test("Paging up to find old threads that were previously read leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
test.slow();
// Given lots of messages in threads are all read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
"Root2",
"Root3",
...msg.manyThreadedOff("Root1", many("T", 20)),
...msg.manyThreadedOff("Root2", many("T", 20)),
...msg.manyThreadedOff("Root3", many("T", 20)),
]);
await util.goTo(room2);
await util.assertRead(room2);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
await util.assertUnreadThread("Root3");
await util.openThread("Root1");
await util.assertReadThread("Root1");
await util.openThread("Root2");
await util.assertReadThread("Root2");
await util.openThread("Root3");
await util.assertReadThread("Root3");
// When I restart and page up to load old thread roots
await util.goTo(room1);
await util.saveAndReload();
await util.goTo(room2);
await util.pageUp();
// Then the room and threads remain read
await util.assertRead(room2);
await util.assertReadThread("Root1");
await util.assertReadThread("Root2");
await util.assertReadThread("Root3");
});
test("Paging up to find old threads that were never read keeps the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
test.slow();
// Given lots of messages in threads that are unread
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
"Root2",
"Root3",
...msg.manyThreadedOff("Root1", many("T", 2)),
...msg.manyThreadedOff("Root2", many("T", 2)),
...msg.manyThreadedOff("Root3", many("T", 2)),
...many("Msg", 100),
]);
await util.goTo(room2);
await util.assertRead(room2);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
await util.assertUnreadThread("Root3");
// When I restart
await util.closeThreadsPanel();
await util.goTo(room1);
await util.saveAndReload();
// Then the room remembers it's read
// TODO: I (andyb) think this will fall in an encrypted room
await util.assertRead(room2);
// And when I page up to load old thread roots
await util.goTo(room2);
await util.pageUp();
// Then the room remains read
await util.assertRead(room2);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
await util.assertUnreadThread("Root3");
});
test("Looking in thread view to find old threads that were never read makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given lots of messages in threads that are unread
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
"Root2",
"Root3",
...msg.manyThreadedOff("Root1", many("T", 2)),
...msg.manyThreadedOff("Root2", many("T", 2)),
...msg.manyThreadedOff("Root3", many("T", 2)),
...many("Msg", 100),
]);
await util.goTo(room2);
await util.assertRead(room2);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
await util.assertUnreadThread("Root3");
// When I restart
await util.closeThreadsPanel();
await util.goTo(room1);
await util.saveAndReload();
// Then the room remembers it's read
// TODO: I (andyb) think this will fall in an encrypted room
await util.assertRead(room2);
// And when I open the threads view
await util.goTo(room2);
await util.openThreadList();
// Then the room remains read
await util.assertRead(room2);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
await util.assertUnreadThread("Root3");
});
test("After marking room as read, paging up to find old threads that were never read leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
test.slow();
// Given lots of messages in threads that are unread but I marked as read on a main timeline message
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
"Root2",
"Root3",
...msg.manyThreadedOff("Root1", many("T", 2)),
...msg.manyThreadedOff("Root2", many("T", 2)),
...msg.manyThreadedOff("Root3", many("T", 2)),
...many("Msg", 100),
]);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// Then the room remembers it's read
await util.assertRead(room2);
// And when I page up to load old thread roots
await util.goTo(room2);
await util.pageUp();
await util.pageUp();
await util.pageUp();
// Then the room remains read
await util.assertStillRead(room2);
await util.assertReadThread("Root1");
await util.assertReadThread("Root2");
await util.assertReadThread("Root3");
});
test("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", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given lots of messages in threads that are unread but I marked as read on a thread message
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
"Root2",
"Root3",
...msg.manyThreadedOff("Root1", many("T1-", 2)),
...msg.manyThreadedOff("Root2", many("T2-", 2)),
...msg.manyThreadedOff("Root3", many("T3-", 2)),
...many("Msg", 100),
msg.threadedOff("Msg0099", "Thread off 99"),
]);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// Then the room remembers it's read
await util.assertRead(room2);
// And when I page up to load old thread roots
await util.goTo(room2);
await util.openThreadList();
// Then the room remains read
await util.assertStillRead(room2);
await util.assertReadThread("Root1");
await util.assertReadThread("Root2");
await util.assertReadThread("Root3");
});
});
});
@@ -0,0 +1,641 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { JSHandle, Locator, Page } from "@playwright/test";
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../element-web-test";
import { type Bot } from "../../pages/bot";
import { type Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
type RoomRef = { name: string; roomId: string };
/**
* Set up for a read receipt test:
* - Create a user with the supplied name
* - As that user, create two rooms with the supplied names
* - Create a bot with the supplied name
* - Invite the bot to both rooms and ensure that it has joined
*/
export const test = base.extend<{
roomAlphaName?: string;
roomAlpha: RoomRef;
roomBetaName?: string;
roomBeta: RoomRef;
msg: MessageBuilder;
util: Helpers;
}>({
displayName: "Mae",
botCreateOpts: { displayName: "Other User" },
roomAlphaName: "Room Alpha",
roomAlpha: async ({ roomAlphaName: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await bot.awaitRoomMembership(roomId);
await use({ name, roomId });
},
roomBetaName: "Room Beta",
roomBeta: async ({ roomBetaName: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await bot.awaitRoomMembership(roomId);
await use({ name, roomId });
},
msg: async ({ page, app, util }, use) => {
await use(new MessageBuilder(page, app, util));
},
util: async ({ roomAlpha, roomBeta, page, app, bot }, use) => {
await use(new Helpers(page, app, bot));
},
});
/**
* A utility that is able to find messages based on their content, by looking
* inside the `timeline` objects in the object model.
*
* Crucially, we hold on to references to events that have been edited or
* redacted, so we can still look them up by their old content.
*
* Provides utilities that build on the ability to find messages, e.g. replyTo,
* which finds a message and then constructs a reply to it.
*/
export class MessageBuilder {
constructor(
private page: Page,
private app: ElementAppPage,
private helpers: Helpers,
) {}
/**
* Map of message content -> event.
*/
messages = new Map<string, Promise<JSHandle<MatrixEvent>>>();
/**
* 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 getMessage(room: JSHandle<Room>, message: string, includeThreads = false): Promise<JSHandle<MatrixEvent>> {
const cached = this.messages.get(message);
if (cached) {
return cached;
}
const promise = room.evaluateHandle(
async (room, { message, includeThreads }) => {
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<MatrixEvent>((resolve) => {
room.on("Room.timeline" as any, (ev: MatrixEvent) => {
if (ev.getContent().body === message) {
resolve(ev);
}
});
});
},
{ message, includeThreads },
);
this.messages.set(message, promise);
return promise;
}
/**
* MessageContentSpec to send an edit into a room
* @param originalMessage - the body of the message to edit
* @param newMessage - the message body to send in the edit
*/
editOf(originalMessage: string, newMessage: string): MessageContentSpec {
return new (class extends MessageContentSpec {
public async getContent(room: JSHandle<Room>): Promise<Record<string, unknown>> {
const ev = await this.messageFinder.getMessage(room, originalMessage, true);
return ev.evaluate((ev, newMessage) => {
// If this event has been redacted, its msgtype will be
// undefined. In that case, we guess msgtype as m.text.
const msgtype = ev.getContent().msgtype ?? "m.text";
return {
"msgtype": msgtype,
"body": `* ${newMessage}`,
"m.new_content": {
msgtype: msgtype,
body: newMessage,
},
"m.relates_to": {
rel_type: "m.replace",
event_id: ev.getId(),
},
};
}, newMessage);
}
})(this);
}
/**
* MessageContentSpec to send a reply into a room
* @param targetMessage - the body of the message to reply to
* @param newMessage - the message body to send into the reply
*/
replyTo(targetMessage: string, newMessage: string): MessageContentSpec {
return new (class extends MessageContentSpec {
public async getContent(room: JSHandle<Room>): Promise<Record<string, unknown>> {
const ev = await this.messageFinder.getMessage(room, targetMessage, true);
return ev.evaluate((ev, newMessage) => {
const threadRel =
ev.getRelation()?.rel_type === "m.thread"
? {
rel_type: "m.thread",
event_id: ev.getRelation().event_id,
}
: {};
return {
"msgtype": "m.text",
"body": newMessage,
"m.relates_to": {
...threadRel,
"m.in_reply_to": {
event_id: ev.getId(),
},
},
};
}, newMessage);
}
})(this);
}
/**
* MessageContentSpec to send a threaded response into a room
* @param rootMessage - the body of the thread root message to send a response to
* @param newMessage - the message body to send into the thread response
*/
threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
return new (class extends MessageContentSpec {
public async getContent(room: JSHandle<Room>): Promise<Record<string, unknown>> {
const ev = await this.messageFinder.getMessage(room, rootMessage);
return ev.evaluate((ev, newMessage) => {
return {
"msgtype": "m.text",
"body": newMessage,
"m.relates_to": {
event_id: ev.getId(),
is_falling_back: true,
rel_type: "m.thread",
},
};
}, newMessage);
}
})(this);
}
/**
* Generate MessageContentSpecs to send multiple threaded responses into a room.
*
* @param rootMessage - the body of the thread root message to send a response to
* @param newMessages - the contents of the messages
*/
manyThreadedOff(rootMessage: string, newMessages: Array<string>): Array<MessageContentSpec> {
return newMessages.map((body) => this.threadedOff(rootMessage, body));
}
/**
* BotActionSpec to send a reaction to an existing event into a room
* @param targetMessage - the body of the message to send a reaction to
* @param reaction - the key of the reaction to send into the room
*/
reactionTo(targetMessage: string, reaction: string): BotActionSpec {
return new (class extends BotActionSpec {
public async performAction(bot: Bot, room: JSHandle<Room>): Promise<void> {
const ev = await this.messageFinder.getMessage(room, targetMessage, true);
const { id, threadId } = await ev.evaluate((ev) => ({
id: ev.getId(),
threadId: !ev.isThreadRoot ? ev.threadRootId : undefined,
}));
const roomId = await room.evaluate((room) => room.roomId);
await bot.reactToMessage(roomId, threadId, id, reaction);
}
})(this);
}
/**
* BotActionSpec to send a redaction into a room
* @param targetMessage - the body of the message to send a redaction to
*/
redactionOf(targetMessage: string): BotActionSpec {
return new (class extends BotActionSpec {
public async performAction(bot: Bot, room: JSHandle<Room>): Promise<void> {
const ev = await this.messageFinder.getMessage(room, targetMessage, true);
const { id, threadId } = await ev.evaluate((ev) => ({
id: ev.getId(),
threadId: !ev.isThreadRoot ? ev.threadRootId : undefined,
}));
const roomId = await room.evaluate((room) => room.roomId);
await bot.redactEvent(roomId, threadId, id);
}
})(this);
}
/**
* Find and display a message.
*
* @param roomRef the ref of the room to look inside
* @param message the content of the message to fine
* @param includeThreads look for messages inside threads, not just the main timeline
*/
async jumpTo(roomRef: RoomRef, message: string, includeThreads = false) {
const room = await this.helpers.findRoomById(roomRef.roomId);
expect(room).toBeTruthy();
const foundMessage = await this.getMessage(room, message, includeThreads);
const roomId = await room.evaluate((room) => room.roomId);
const foundMessageId = await foundMessage.evaluate((ev) => ev.getId());
await this.page.goto(`/#/room/${roomId}/${foundMessageId}`);
}
async sendThreadedReadReceipt(room: JSHandle<Room>, targetMessage: string) {
const event = await this.getMessage(room, targetMessage, true);
await this.app.client.evaluate(
(client, { event }) => {
return client.sendReadReceipt(event);
},
{ event },
);
}
async sendUnthreadedReadReceipt(room: JSHandle<Room>, targetMessage: string) {
const event = await this.getMessage(room, targetMessage, true);
await this.app.client.evaluate(
(client, { event }) => {
return client.sendReadReceipt(event, "m.read" as any as ReceiptType, true);
},
{ event },
);
}
}
/**
* Something that can provide the content of a message.
*
* For example, we return and instance of this from {@link
* MessageBuilder.replyTo} which creates a reply based on a previous message.
*/
export abstract class MessageContentSpec {
messageFinder: MessageBuilder | null;
constructor(messageFinder: MessageBuilder = null) {
this.messageFinder = messageFinder;
}
public abstract getContent(room: JSHandle<Room>): Promise<Record<string, unknown>>;
}
/**
* Something that can perform an action at the time we would usually send a
* message.
*
* For example, we return an instance of this from {@link
* MessageBuilder.redactionOf} which redacts the message we are referring to.
*/
export abstract class BotActionSpec {
messageFinder: MessageBuilder | null;
constructor(messageFinder: MessageBuilder = null) {
this.messageFinder = messageFinder;
}
public abstract performAction(client: Client, room: JSHandle<Room>): Promise<void>;
}
/**
* Something that we will turn into a message or event when we pass it in to
* e.g. receiveMessages.
*/
export type Message = string | MessageContentSpec | BotActionSpec;
class Helpers {
constructor(
private page: Page,
private app: ElementAppPage,
private bot: Bot,
) {}
/**
* Use the supplied client to send messages or perform actions as specified by
* the supplied {@link Message} items.
*/
async sendMessageAsClient(cli: Client, roomRef: RoomRef, messages: Message[]) {
const roomId = roomRef.roomId;
const room = await this.findRoomById(roomId);
expect(room).toBeTruthy();
for (const message of messages) {
if (typeof message === "string") {
await cli.sendMessage(roomId, { body: message, msgtype: "m.text" });
} else if (message instanceof MessageContentSpec) {
await cli.sendMessage(roomId, await message.getContent(room));
} else {
await message.performAction(cli, room);
}
// TODO: without this wait, some tests that send lots of messages flake
// from time to time. I (andyb) have done some investigation, but it
// needs more work to figure out. The messages do arrive over sync, but
// they never appear in the timeline, and they never fire a
// Room.timeline event. I think this only happens with events that refer
// to other events (e.g. replies), so it might be caused by the
// referring event arriving before the referred-to event.
await this.page.waitForTimeout(100);
}
}
/**
* Open the room with the supplied name.
*/
async goTo(room: RoomRef) {
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
}
/**
* Expand the message with the supplied index in the timeline.
* @param index
*/
async openCollapsedMessage(index: number) {
const button = this.page.locator(".mx_GenericEventListSummary_toggle");
await button.nth(index).click();
}
/**
* Click the thread with the supplied content in the thread root to open it in
* the Threads panel.
*/
async openThread(rootMessage: string) {
const tile = this.page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: rootMessage });
await tile.hover();
await tile.getByRole("button", { name: "Reply in thread" }).click();
await expect(this.page.locator(".mx_ThreadView_timelinePanelWrapper")).toBeVisible();
}
/**
* Close the threads panel.
*/
async closeThreadsPanel() {
await this.page.locator(".mx_RightPanel").getByTestId("base-card-close-button").click();
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
}
/**
* Return to the list of threads, given we are viewing a single thread.
*/
async backToThreadsList() {
await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click();
}
/**
* Assert that the message containing the supplied text is visible in the UI.
* Note: matches part of the message content as well as the whole of it.
*/
async assertMessageLoaded(messagePart: string) {
await expect(this.page.locator(".mx_EventTile_body").getByText(messagePart)).toBeVisible();
}
/**
* Assert that the message containing the supplied text is not visible in the UI.
* Note: matches part of the message content as well as the whole of it.
*/
async assertMessageNotLoaded(messagePart: string) {
await expect(this.page.locator(".mx_EventTile_body").getByText(messagePart)).not.toBeVisible();
}
/**
* Scroll the messages panel up 1000 pixels.
*/
async pageUp() {
await this.page.locator(".mx_RoomView_messagePanel").evaluateAll((messagePanels) => {
messagePanels.forEach((messagePanel) => (messagePanel.scrollTop -= 1000));
});
}
getRoomListTile(label: string) {
return this.page.getByRole("option", { name: new RegExp("^Open room " + label) });
}
/**
* Click the "Mark as Read" context menu item on the room with the supplied name
* in the room list.
*/
async markAsRead(room: RoomRef) {
await this.getRoomListTile(room.name).click({ button: "right" });
await this.page.getByText("Mark as read").click();
}
/**
* Assert that the room with the supplied name is "read" in the room list - i.g.
* has not dot or count of unread messages.
*/
async assertRead(room: RoomRef) {
const tile = this.getRoomListTile(room.name);
await expect(tile.getByTestId("notification-decoration")).not.toBeVisible();
await expect(tile).not.toHaveAccessibleName(/with \d* unread message/);
}
/**
* Assert that this room remains read, when it was previously read.
* (In practice, this just waits a short while to allow any unread marker to
* appear, and then asserts that the room is read.)
*/
async assertStillRead(room: RoomRef) {
await this.page.waitForTimeout(200);
await this.assertRead(room);
}
/**
* 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
*/
async assertUnread(room: RoomRef, count: number) {
const tile = this.getRoomListTile(room.name);
await expect(tile).toBeVisible();
await expect(tile).toHaveAccessibleName(/with \d* unread message/);
}
async unreadCountForRoomTile(tile: Locator): Promise<number> {
const accessibleName = await tile.getAttribute("aria-label");
const match = accessibleName?.match(/(\d+)\s+unread message/);
return match ? parseInt(match[1], 10) : 0;
}
/**
* Assert a given room is marked as unread, and the number of unread
* messages is less than the supplied count.
*
* @param room - the name of the room to check
* @param lessThan - the number of unread messages that is too many
*/
async assertUnreadLessThan(room: RoomRef, lessThan: number) {
const tile = this.getRoomListTile(room.name);
// https://playwright.dev/docs/test-assertions#expectpoll
// .toBeLessThan doesn't have a retry mechanism, so we use .poll
await expect
.poll(async () => {
return this.unreadCountForRoomTile(tile);
})
.toBeLessThan(lessThan);
}
/**
* Assert a given room is marked as unread, and the number of unread
* messages is greater than the supplied count.
*
* @param room - the name of the room to check
* @param greaterThan - the number of unread messages that is too few
*/
async assertUnreadGreaterThan(room: RoomRef, greaterThan: number) {
const tile = this.getRoomListTile(room.name);
// https://playwright.dev/docs/test-assertions#expectpoll
// .toBeGreaterThan doesn't have a retry mechanism, so we use .poll
await expect
.poll(async () => {
return this.unreadCountForRoomTile(tile);
})
.toBeGreaterThan(greaterThan);
}
/**
* Click the "Threads" or "Back" button if needed to get to the threads list.
*/
async openThreadList() {
// If we've just entered the room, the threads panel takes a while to decide
// whether it's open or not - wait here to give it a chance to settle.
await this.page.waitForTimeout(200);
const threadPanel = this.page.locator(".mx_ThreadPanel");
const isThreadPanelOpen = (await threadPanel.count()) !== 0;
if (!isThreadPanelOpen) {
await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click();
}
await expect(threadPanel).toBeVisible();
await threadPanel.evaluate(($panel) => {
const $button = $panel.querySelector<HTMLElement>('[data-testid="base-card-back-button"]');
const title = $panel.querySelector<HTMLElement>(".mx_BaseCard_header_title")?.textContent;
// If the Threads back button is present then click it - the
// threads button can open either threads list or thread panel
if ($button && title !== "Threads") {
$button.click();
}
});
}
async findRoomById(roomId: string): Promise<JSHandle<Room>> {
return this.app.client.evaluateHandle((cli, roomId) => {
return cli.getRooms().find((r) => r.roomId === roomId);
}, roomId);
}
private async getThreadListTile(rootMessage: string) {
await this.openThreadList();
return this.page.locator(".mx_ThreadPanel li", { hasText: rootMessage });
}
/**
* Assert that the thread with the supplied content in its root message is shown
* as read in the Threads list.
*/
async assertReadThread(rootMessage: string) {
const tile = await this.getThreadListTile(rootMessage);
await expect(tile.locator(".mx_NotificationBadge")).not.toBeVisible();
}
/**
* Assert that the thread with the supplied content in its root message is shown
* as unread in the Threads list.
*/
async assertUnreadThread(rootMessage: string) {
const tile = await this.getThreadListTile(rootMessage);
await expect(tile.locator(".mx_NotificationBadge")).toBeVisible();
}
/**
* Save our indexeddb information and then refresh the page.
*/
async saveAndReload() {
await this.app.client.evaluate((cli) => {
// @ts-ignore
return (cli.store as IndexedDBStore).reallySave();
});
await this.page.reload();
// Wait for the app to reload
await expect(this.page.locator(".mx_RoomView")).toBeVisible();
}
/**
* Sends messages into given room as a bot
* @param room - the name of the room to send messages into
* @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf`
*/
async receiveMessages(room: RoomRef, messages: Message[]) {
await this.sendMessageAsClient(this.bot, room, messages);
}
/**
* Open the room list menu
*/
async toggleRoomListMenu() {
const tile = this.getRoomListTile("Rooms");
await tile.hover();
const button = tile.getByLabel("List options");
await button.click();
}
/**
* Assert that the room list is ordered as expected
* @param rooms
*/
async assertRoomListOrder(rooms: Array<{ name: string }>) {
const roomListContainer = this.page.getByTestId("room-list");
const roomTiles = roomListContainer.getByRole("option");
for (const [i, room] of rooms.entries()) {
await expect(roomTiles.nth(i)).toHaveAccessibleName(new RegExp(`${room.name}`));
}
}
}
/**
* BotActionSpec to send a custom event
* @param eventType - the type of the event to send
* @param content - the event content to send
*/
export function customEvent(eventType: string, content: Record<string, any>): BotActionSpec {
return new (class extends BotActionSpec {
public async performAction(cli: Client, room: JSHandle<Room>): Promise<void> {
const roomId = await room.evaluate((room) => room.roomId);
await cli.sendEvent(roomId, null, eventType, content);
}
})();
}
/**
* Generate strings with the supplied prefix, suffixed with numbers.
*
* @param prefix the prefix of each string
* @param howMany the number of strings to generate
*/
export function many(prefix: string, howMany: number): Array<string> {
return Array.from(Array(howMany).keys()).map((i) => prefix + i.toString().padStart(4, "0"));
}
export { expect };
@@ -0,0 +1,42 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Message ordering", () => {
test.describe("in the main timeline", () => {
test.fixme("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {});
test.fixme("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {});
});
test.describe("in threads", () => {
// These don't pass yet - we need MSC4033 - we don't even know the Sync order yet
test.fixme("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {});
test.fixme("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.fixme("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {});
test.fixme("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {});
test.fixme("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {});
test.fixme("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {});
test.fixme("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {});
test.fixme("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {});
});
test.describe("thread roots", () => {
test.fixme("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {});
test.fixme("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {});
test.fixme("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {});
test.fixme("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {});
});
});
});
@@ -0,0 +1,53 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("messages with missing referents", () => {
test.fixme("A message in an unknown thread is not visible and the room is read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread existed and the room is read
await util.goTo(room1);
await util.receiveMessages(room2, ["Root1", msg.threadedOff("Root1", "T1a")]);
// When I restart, forgetting the thread root
// And I receive a message on that thread
// Then the message is invisible and the room remains read
});
test.fixme("When a message's thread root appears later the thread appears and the room is unread", () => {});
test.fixme("An edit of an unknown message is not visible and the room is read", () => {});
test.fixme("When an edit's message appears later the edited version appears and the room is unread", () => {});
test.fixme("A reaction to an unknown message is not visible and the room is read", () => {});
test.fixme("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.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.fixme("A receipt for an unknown message does not change the state of an unread room", () => {});
test.fixme("A receipt for an unknown message does not change the state of a read room", () => {});
test.fixme("A threaded receipt for an unknown message does not change the state of an unread thread", () => {});
test.fixme("A threaded receipt for an unknown message does not change the state of a read thread", () => {});
test.fixme("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {});
test.fixme("A threaded receipt for an unknown thread does not change the state of a read thread", () => {});
test.fixme("A threaded receipt for a message on main does not change the state of an unread room", () => {});
test.fixme("A threaded receipt for a message on main does not change the state of a read room", () => {});
test.fixme("A main receipt for a message on a thread does not change the state of an unread room", () => {});
test.fixme("A main receipt for a message on a thread does not change the state of a read room", () => {});
test.fixme("A threaded receipt for a thread root does not mark it as read", () => {});
// Harder: validate that we request the messages we are missing?
});
});
@@ -0,0 +1,304 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { many, test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("new messages", () => {
test.describe("in threads", () => {
test("Receiving a message makes a room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message arrived and is read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When I receive a threaded message
await util.receiveMessages(room2, [msg.threadedOff("Msg1", "Resp1")]);
// Then the room stays read
await util.assertRead(room2);
// but the thread is unread
await util.goTo(room2);
await util.assertUnreadThread("Msg1");
});
test("Reading the last threaded message makes the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists and is not read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
// When I read it
await util.openThread("Msg1");
// The thread becomes read
await util.assertReadThread("Msg1");
});
test("Reading a thread message makes the thread read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg1", "Resp2"),
]);
await util.assertUnread(room2, 1); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
// Then room is read
await util.assertRead(room2);
// Reading the thread causes it to become read too
await util.openThread("Msg1");
await util.assertReadThread("Msg1");
await util.assertRead(room2);
});
test("Reading an older thread message leaves the thread unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given there are many messages in a thread
await util.goTo(room1);
await util.receiveMessages(room2, [
"ThreadRoot",
...msg.manyThreadedOff("ThreadRoot", many("InThread", 20)),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertUnreadThread("ThreadRoot");
await util.goTo(room1);
// When I read an older message in the thread
await msg.jumpTo(room2, "InThread0000", true);
// Then the thread is still marked as unread
await util.backToThreadsList();
await util.assertUnreadThread("ThreadRoot");
});
test("Reading only one thread's message makes that thread read but not others", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have unread threads
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
"Msg2",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg2", "Resp2"),
]);
await util.assertUnread(room2, 2); // (Sanity)
await util.goTo(room2);
await util.assertRead(room2);
await util.assertUnreadThread("Msg1");
await util.assertUnreadThread("Msg2");
// When I read one of them
await util.openThread("Msg1");
// Then that one is read, but the other is not
await util.assertReadThread("Msg1");
await util.assertUnreadThread("Msg2");
});
test("Reading the main timeline does not mark a thread message as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg1", "Resp2"),
]);
await util.assertUnread(room2, 1); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
await util.assertRead(room2);
// Then thread does appear unread
await util.assertUnreadThread("Msg1");
});
test("Marking a room with unread threads as read makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have an unread thread
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg1", "Resp2"),
]);
await util.assertUnread(room2, 1); // (Sanity)
// When I mark the room as read
await util.markAsRead(room2);
// Then the room is read
await util.assertRead(room2);
// and so are the threads
await util.assertReadThread("Msg1");
});
test("Sending a new thread message after marking as read makes it unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg1", "Resp2"),
]);
// When I mark the room as read
await util.markAsRead(room2);
await util.assertRead(room2);
// Then another message appears in the thread
await util.receiveMessages(room2, [msg.threadedOff("Msg1", "Resp3")]);
// Then the thread becomes unread
await util.goTo(room2);
await util.assertUnreadThread("Msg1");
});
test("Sending a new different-thread message after marking as read makes it unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given 2 threads exist, and Thread2 has the latest message in it
await util.goTo(room1);
await util.receiveMessages(room2, ["Thread1", "Thread2", msg.threadedOff("Thread1", "t1a")]);
// Make sure the message in Thread 1 has definitely arrived, so that we know for sure
// that the one in Thread 2 is the latest.
await util.receiveMessages(room2, [msg.threadedOff("Thread2", "t2a")]);
// When I mark the room as read (making an unthreaded receipt for t2a)
await util.markAsRead(room2);
await util.assertRead(room2);
// Then another message appears in the other thread
await util.receiveMessages(room2, [msg.threadedOff("Thread1", "t1b")]);
// Then the other thread becomes unread
await util.goTo(room2);
await util.assertUnreadThread("Thread1");
});
test("A room with a new threaded message is still unread after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg1", "Resp2"),
]);
await util.assertUnread(room2, 1); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
// Then room appears read
await util.assertRead(room2);
/// but with an unread thread
await util.assertUnreadThread("Msg1");
await util.saveAndReload();
await util.assertRead(room2);
await util.goTo(room2);
await util.assertUnreadThread("Msg1");
// Opening the thread now marks it as read
await util.openThread("Msg1");
await util.assertReadThread("Msg1");
});
test("A room where all threaded messages are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have read all the threads
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.threadedOff("Msg1", "Resp2"),
]);
await util.assertUnread(room2, 1); // (Sanity)
await util.goTo(room2);
await util.assertRead(room2);
await util.assertUnreadThread("Msg1");
await util.openThread("Msg1");
await util.assertReadThread("Msg1");
// When I restart
await util.saveAndReload();
// Then the room & thread still read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Msg1");
});
});
});
});
@@ -0,0 +1,163 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { many, test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("new messages", () => {
test.describe("in the main timeline", () => {
test("Receiving a message makes a room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I am in a different room
await util.goTo(room1);
await util.assertRead(room2);
// When I receive some messages
await util.receiveMessages(room2, ["Msg1"]);
// Then the room is marked as unread
await util.assertUnread(room2, 1);
});
test("Reading latest message makes the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have some unread messages
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
// When I read the main timeline
await util.goTo(room2);
// Then the room becomes read
await util.assertRead(room2);
});
test("Reading an older message leaves the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given there are lots of messages in a room
await util.goTo(room1);
await util.receiveMessages(room2, many("Msg", 30));
await util.assertUnread(room2, 30);
// When I jump to one of the older messages
await msg.jumpTo(room2, "Msg0001");
// Then the room is still unread, but some messages were read
await util.assertUnreadLessThan(room2, 30);
});
test("Marking a room as read makes it read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
// Given I have some unread messages
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
// When I mark the room as read
await util.markAsRead(room2);
// Then it is read
await util.assertRead(room2);
});
test("Receiving a new message after marking as read makes it unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have marked my messages as read
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I receive a new message
await util.receiveMessages(room2, ["Msg2"]);
// Then the room is unread
await util.assertUnread(room2, 1);
});
test("A room with a new message is still unread after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have an unread message
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
// When I restart
await util.saveAndReload();
// Then I still have an unread message
await util.assertUnread(room2, 1);
});
test("A room where all messages are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have read all messages
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// Then all messages are still read
await util.assertRead(room2);
});
test("A room that was marked as read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have marked all messages as read
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// Then all messages are still read
await util.assertRead(room2);
});
});
});
});
@@ -0,0 +1,113 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { many, test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("new messages", () => {
test.describe("thread roots", () => {
test("Reading a thread root does not mark the thread as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertUnread(room2, 1); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
// Then room doesn't appear unread but the thread does
await util.assertRead(room2);
await util.assertUnreadThread("Msg1");
});
test("Reading a thread root within the thread view marks it as read in the main timeline", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given lots of messages are on the main timeline, and one has a thread off it
await util.goTo(room1);
await util.receiveMessages(room2, [
...many("beforeThread", 30),
"ThreadRoot",
msg.threadedOff("ThreadRoot", "InThread"),
...many("afterThread", 30),
]);
await util.assertUnread(room2, 61); // Sanity
// When I jump to an old message and read the thread
await msg.jumpTo(room2, "beforeThread0000");
// When the thread is opened, the timeline is scrolled until the thread root reached the center
await util.openThread("ThreadRoot");
// Then the thread root is marked as read in the main timeline,
// 30 remaining messages are unread - 6 messages are displayed under the thread root
await util.assertUnread(room2, 30 - 6);
});
test("Creating a new thread based on a reply makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message and reply exist and are read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
await util.assertRead(room2);
// When I receive a thread message created on the reply
await util.receiveMessages(room2, [msg.threadedOff("Reply1", "Resp1")]);
// Then the thread is unread
await util.goTo(room2);
await util.assertUnreadThread("Reply1");
});
test("Reading a thread whose root is a reply makes the thread read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread thread off a reply exists
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.replyTo("Msg1", "Reply1"),
msg.threadedOff("Reply1", "Resp1"),
]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.assertRead(room2);
await util.assertUnreadThread("Reply1");
// When I read the thread
await util.openThread("Reply1");
// Then the room and thread are read
await util.assertRead(room2);
await util.assertReadThread("Reply1");
});
});
});
});
@@ -0,0 +1,33 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Notifications", () => {
test.describe("in the main timeline", () => {
test.fixme("A new message that mentions me shows a notification", () => {});
test.fixme("Reading a notifying message reduces the notification count in the room list, space and tab", () => {});
test.fixme("Reading the last notifying message removes the notification marker from room list, space and tab", () => {});
test.fixme("Editing a message to mentions me shows a notification", () => {});
test.fixme("Reading the last notifying edited message removes the notification marker", () => {});
test.fixme("Redacting a notifying message removes the notification marker", () => {});
});
test.describe("in threads", () => {
test.fixme("A new threaded message that mentions me shows a notification", () => {});
test.fixme("Reading a notifying threaded message removes the notification count", () => {});
test.fixme("Notification count remains steady when reading threads that contain seen notifications", () => {});
test.fixme("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {});
test.fixme("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {});
test.fixme("Redacting a notifying threaded message removes the notification marker", () => {});
});
});
});
@@ -0,0 +1,200 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test, expect } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("reactions", () => {
test.describe("in threads", () => {
test("A reaction to a threaded message does not make the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists and I have read it
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.assertReadThread("Msg1");
await util.goTo(room1);
// When someone reacts to a thread message
await util.receiveMessages(room2, [msg.reactionTo("Reply1", "🪿")]);
// Then the room remains read
await util.assertStillRead(room2);
await util.assertReadThread("Msg1");
});
test("Marking a room as read after a reaction in a thread makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists with a reaction
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Reply1"),
msg.reactionTo("Reply1", "🪿"),
]);
await util.assertUnread(room2, 1);
// When I mark the room as read
await util.markAsRead(room2);
// Then it becomes read
await util.assertRead(room2);
});
test("Reacting to a thread message after marking as read does not make the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists and I have marked it as read
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When someone reacts to a thread message
await util.receiveMessages(room2, [msg.reactionTo("Reply1", "🪿")]);
// Then the room remains read
await util.assertStillRead(room2);
// as does the thread
await util.assertReadThread("Msg1");
});
test("A room with a reaction to a threaded message is still unread after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists and I have read it
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1);
// And someone reacted to it, which doesn't make it read
await util.receiveMessages(room2, [msg.reactionTo("Reply1", "🪿")]);
await util.assertStillRead(room2);
await util.assertReadThread("Msg1");
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
await util.assertReadThread("Msg1");
});
test("A room where all reactions in threads are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given multiple threads with reactions exist and are read
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Reply1a"),
msg.reactionTo("Reply1a", "r"),
"Msg2",
msg.threadedOff("Msg1", "Reply1b"),
msg.threadedOff("Msg2", "Reply2a"),
msg.reactionTo("Msg1", "e"),
msg.threadedOff("Msg2", "Reply2b"),
msg.reactionTo("Reply2a", "a"),
msg.reactionTo("Reply2b", "c"),
msg.reactionTo("Reply1b", "t"),
]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertReadThread("Msg1");
await util.openThread("Msg2");
await util.assertReadThread("Msg2");
await util.assertRead(room2);
await util.goTo(room1);
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Msg1");
await util.assertReadThread("Msg2");
});
test("Can remove a reaction in a thread", async ({
page,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Note: this is not strictly a read receipt test, but it checks
// for a bug we caused when we were fixing unreads, so it's
// included here. The bug is:
// https://github.com/vector-im/element-web/issues/26498
// Given a thread exists
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1a")]);
await util.assertUnread(room2, 1);
// When I react to a thread message
await util.goTo(room2);
await util.openThread("Msg1");
await page.locator(".mx_ThreadPanel").getByText("Reply1a").hover();
await page.getByRole("button", { name: "React" }).click();
await page.locator(".mx_EmojiPicker_body").getByText("😀").click();
// And cancel the reaction
await page.locator(".mx_ThreadPanel").getByLabel("Mae reacted with 😀").click();
// Then it disappears
await expect(page.locator(".mx_ThreadPanel").getByLabel("Mae reacted with 😀")).not.toBeVisible();
// And I can do it all again without an error
await page.locator(".mx_ThreadPanel").getByText("Reply1a").hover();
await page.getByRole("button", { name: "React" }).click();
await page.locator(".mx_EmojiPicker_body").getByText("😀").first().click();
await page.locator(".mx_ThreadPanel").getByLabel("Mae reacted with 😀").click();
await expect(await page.locator(".mx_ThreadPanel").getByLabel("Mae reacted with 😀")).not.toBeVisible();
});
});
});
});
@@ -0,0 +1,94 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("reactions", () => {
test.describe("in the main timeline", () => {
test("Receiving a reaction to a message does not make a room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
// When I read the main timeline
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]);
await util.assertRead(room2);
});
test("Reacting to a message after marking as read does not make the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]);
await util.assertRead(room2);
});
test("A room with an unread reaction is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]);
await util.assertRead(room2);
await util.saveAndReload();
await util.assertRead(room2);
});
test("A room where all reactions are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", "Msg2", msg.reactionTo("Msg2", "🪿")]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
await util.saveAndReload();
await util.assertRead(room2);
});
});
});
});
@@ -0,0 +1,109 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("reactions", () => {
test.describe("thread roots", () => {
test("A reaction to a thread root does not make the room unread", async ({
page,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a read thread root exists
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.assertReadThread("Msg1");
// When someone reacts to it
await util.goTo(room1);
await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]);
await page.waitForTimeout(200);
// Then the room is still read
await util.assertRead(room2);
// as is the thread
await util.assertReadThread("Msg1");
});
test("Reading a reaction to a thread root leaves the room read", async ({
page,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a read thread root exists
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
// And the reaction to it does not make us unread
await util.goTo(room1);
await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]);
await util.assertRead(room2);
await util.assertReadThread("Msg1");
// When we read the reaction and go away again
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1);
await page.waitForTimeout(200);
// Then the room is still read
await util.assertRead(room2);
await util.assertReadThread("Msg1");
});
test("Reacting to a thread root after marking as read makes the room unread but not the thread", async ({
page,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread root exists
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
await util.assertUnread(room2, 1);
// And we have marked the room as read
await util.markAsRead(room2);
await util.assertRead(room2);
await util.assertReadThread("Msg1");
// When someone reacts to it
await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]);
await page.waitForTimeout(200);
// Then the room is still read
await util.assertRead(room2);
// as is the thread
await util.assertReadThread("Msg1");
});
});
});
});
@@ -0,0 +1,336 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { JSHandle } from "@playwright/test";
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
import { expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Bot } from "../../pages/bot";
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.use({
displayName: "Mae",
botCreateOpts: { displayName: "Other User" },
});
const selectedRoomName = "Selected Room";
const otherRoomName = "Other Room";
let otherRoomId: string;
let selectedRoomId: string;
const sendMessage = async (bot: Bot, no = 1): Promise<ISendEventResponse> => {
return bot.sendMessage(otherRoomId, { body: `Message ${no}`, msgtype: "m.text" });
};
const botSendThreadMessage = (bot: Bot, threadId: string): Promise<ISendEventResponse> => {
return bot.sendEvent(otherRoomId, threadId, "m.room.message", { body: "Message", msgtype: "m.text" });
};
const fakeEventFromSent = (
app: ElementAppPage,
eventResponse: ISendEventResponse,
threadRootId: string | undefined,
): Promise<JSHandle<MatrixEvent>> => {
return app.client.evaluateHandle(
(client, { otherRoomId, eventResponse, threadRootId }) => {
return {
getRoomId: () => otherRoomId,
getId: () => eventResponse.event_id,
threadRootId,
getTs: () => 1,
isRelation: (relType) => {
return !relType || relType === "m.thread";
},
} as any as MatrixEvent;
},
{ otherRoomId, eventResponse, threadRootId },
);
};
/**
* Send a threaded receipt marking the message referred to in
* eventResponse as read. If threadRootEventResponse is supplied, the
* receipt will have its event_id as the thread root ID for the receipt.
*/
const sendThreadedReadReceipt = async (
app: ElementAppPage,
eventResponse: ISendEventResponse,
threadRootEventResponse: ISendEventResponse = undefined,
) => {
await app.client.sendReadReceipt(
await fakeEventFromSent(app, eventResponse, threadRootEventResponse?.event_id),
);
};
/**
* Send an unthreaded receipt marking the message referred to in
* eventResponse as read.
*/
const sendUnthreadedReadReceipt = async (app: ElementAppPage, eventResponse: ISendEventResponse) => {
await app.client.sendReadReceipt(
await fakeEventFromSent(app, eventResponse, undefined),
"m.read" as any as ReceiptType,
true,
);
};
test.beforeEach(async ({ page, app, user, bot }) => {
/*
* 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.
*/
selectedRoomId = await app.client.createRoom({ name: selectedRoomName });
// Invite the bot to Other room
otherRoomId = await app.client.createRoom({ name: otherRoomName, invite: [bot.credentials.userId] });
await page.goto(`/#/room/${otherRoomId}`);
await expect(page.getByText(`${bot.credentials.displayName} joined the room`)).toBeVisible();
// Then go into Selected room
await page.goto(`/#/room/${selectedRoomId}`);
});
test("With sync accumulator, considers main thread and unthreaded receipts #24629", async ({ page, app, bot }) => {
// Details are in https://github.com/vector-im/element-web/issues/24629
// This proves we've fixed one of the "stuck unreads" issues.
// Given we sent 3 events on the main thread
await sendMessage(bot);
const main2 = await sendMessage(bot);
const main3 = await sendMessage(bot);
// (So the room starts off unread)
await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the last event in main
// And an unthreaded receipt for an earlier event
await sendThreadedReadReceipt(app, main3);
await sendUnthreadedReadReceipt(app, main2);
// (So the room has no unreads)
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
// And we persuade the app to persist its state to indexeddb by reloading and waiting
await page.reload();
await expect(page.getByLabel(`${selectedRoomName}`)).toBeVisible();
// And we reload again, fetching the persisted state FROM indexeddb
await page.reload();
// Then the room is read, because the persisted state correctly remembers both
// receipts. (In #24629, the unthreaded receipt overwrote the main thread one,
// meaning that the room still said it had unread messages.)
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
await expect(page.getByLabel(`${otherRoomName} Unread messages.`)).not.toBeVisible();
});
test("Recognises unread messages on main thread after receiving a receipt for earlier ones", async ({
page,
app,
bot,
}) => {
// Given we sent 3 events on the main thread
await sendMessage(bot);
const main2 = await sendMessage(bot);
await sendMessage(bot);
// (The room starts off unread)
await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the second-last event in main
await sendThreadedReadReceipt(app, main2);
// Then the room has only one unread
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
});
test("Considers room read if there is only a main thread and we have a main receipt", async ({
page,
app,
bot,
}) => {
// Given we sent 3 events on the main thread
await sendMessage(bot);
await sendMessage(bot);
const main3 = await sendMessage(bot);
// (The room starts off unread)
await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the last event in main
await sendThreadedReadReceipt(app, main3);
// Then the room has no unreads
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
});
test("Recognises unread messages on other thread after receiving a receipt for earlier ones", async ({
page,
app,
bot,
util,
}) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
const thread1a = await botSendThreadMessage(bot, main1.event_id);
await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread that aren't shown
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
// When we send receipts for main, and the second-last in the thread
await sendThreadedReadReceipt(app, main1);
await sendThreadedReadReceipt(app, thread1a, main1);
// Then the room has only one unread - the one in the thread
await util.goTo({ name: otherRoomName, roomId: otherRoomId });
await util.assertUnreadThread("Message 1");
});
test("Considers room read if there are receipts for main and other thread", async ({ page, app, bot, util }) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
await botSendThreadMessage(bot, main1.event_id);
const thread1b = await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread which don't show
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
// When we send receipts for main, and the last in the thread
await sendThreadedReadReceipt(app, main1);
await sendThreadedReadReceipt(app, thread1b, main1);
// Then the room has no unreads
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
await util.goTo({ name: otherRoomName, roomId: otherRoomId });
await util.assertReadThread("Message 1");
});
test("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", async ({
page,
app,
bot,
util,
}) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
const thread1a = await botSendThreadMessage(bot, main1.event_id);
await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread which don't count
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
// When we send an unthreaded receipt for the second-last in the thread
await sendUnthreadedReadReceipt(app, thread1a);
// Then the room has only one unread - the one in the
// thread. The one in main is read because the unthreaded
// receipt is for a later event. The room should therefore be
// read, and the thread unread.
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
await util.goTo({ name: otherRoomName, roomId: otherRoomId });
await util.assertUnreadThread("Message 1");
});
test("Recognises unread messages on main after receiving a unthreaded receipt for a thread message", async ({
page,
app,
bot,
}) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
await botSendThreadMessage(bot, main1.event_id);
const thread1b = await botSendThreadMessage(bot, main1.event_id);
await sendMessage(bot);
// 2 unreads on the main thread, 2 in the new thread which don't count
await expect(page.getByLabel(`${otherRoomName} with 2 unread messages.`)).toBeVisible();
// When we send an unthreaded receipt for the last in the thread
await sendUnthreadedReadReceipt(app, thread1b);
// Then the room has only one unread - the one in the
// main thread, because it is later than the unthreaded
// receipt.
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
});
/**
* The idea of this test is to intercept the receipt / read read_markers requests and
* assert that the correct ones are sent.
* Prose playbook:
* - Another user sends enough messages that the timeline becomes scrollable
* - The current user looks at the room and jumps directly to the first unread message
* - At this point, a receipt for the last message in the room and
* a fully read marker for the last visible message are expected to be sent
* - Then the user jumps to the end of the timeline
* - A fully read marker for the last message in the room is expected to be sent
*/
test("Should send the correct receipts", async ({ page, bot }) => {
const uriEncodedOtherRoomId = encodeURIComponent(otherRoomId);
const receiptRequestPromise = page.waitForRequest(
new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/receipt/m\\.read/.+`),
);
const numberOfMessages = 20;
const sendMessageResponses: ISendEventResponse[] = [];
for (let i = 1; i <= numberOfMessages; i++) {
sendMessageResponses.push(await sendMessage(bot, i));
}
const lastMessageId = sendMessageResponses.at(-1).event_id;
const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
// wait until all messages have been received
await expect(
page.getByLabel(`${otherRoomName} with ${sendMessageResponses.length} unread messages.`),
).toBeVisible();
// switch to the room with the messages
await page.goto(`/#/room/${otherRoomId}`);
const receiptRequest = await receiptRequestPromise;
// assert the read receipt for the last message in the room
expect(receiptRequest.url()).toContain(uriEncodedLastMessageId);
expect(receiptRequest.postDataJSON()).toEqual({
thread_id: "main",
});
// the following code tests the fully read marker somewhere in the middle of the room
const readMarkersRequestPromise = page.waitForRequest(
new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/read_markers`),
);
await page.getByRole("button", { name: "Jump to first unread message." }).click();
const readMarkersRequest = await readMarkersRequestPromise;
// since this is not pixel perfect,
// the fully read marker should be +/- 1 around the last visible message
expect([
sendMessageResponses[11].event_id,
sendMessageResponses[12].event_id,
sendMessageResponses[13].event_id,
]).toContain(readMarkersRequest.postDataJSON()["m.fully_read"]);
// the following code tests the fully read marker at the bottom of the room
const readMarkersRequestPromise2 = page.waitForRequest(
new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/read_markers`),
);
await page.getByRole("button", { name: "Scroll to most recent messages" }).click();
const readMarkersRequest2 = await readMarkersRequestPromise2;
expect(readMarkersRequest2.postDataJSON()).toEqual({
["m.fully_read"]: sendMessageResponses.at(-1).event_id,
});
});
});
@@ -0,0 +1,20 @@
# High Level Read Receipt Tests
Tips for writing these tests:
- Break up your tests into the smallest test case possible. The purpose of
these tests is to understand hard-to-find bugs, so small tests are necessary.
We know that Playwright recommends combining tests together for performance, but
that will frustrate our goals here. (We will need to find a different way to
reduce CI time.)
- Try to assert something after every action, to make sure it has completed.
E.g.:
markAsRead(room2);
assertRead(room2);
You should especially follow this rule if you are jumping to a different
room or similar straight afterward.
- Use assertStillRead() if you are asserting something is read when it was
also read before. This waits a little while to make sure you're not getting a
false positive.
@@ -0,0 +1,557 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("redactions", () => {
test.describe("in threads", () => {
test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have some threads
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
msg.threadedOff("Root1", "ThreadMsg1"),
msg.threadedOff("Root1", "ThreadMsg2"),
"Root2",
msg.threadedOff("Root2", "Root2->A"),
]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
// And I have read them
await util.assertUnreadThread("Root1");
await util.openThread("Root1");
await util.assertRead(room2);
await util.backToThreadsList();
await util.assertReadThread("Root1");
await util.openThread("Root2");
await util.assertReadThread("Root2");
await util.closeThreadsPanel();
await util.goTo(room1);
// When the latest message in a thread is redacted
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
// Then the room and thread are still read
await util.assertStillRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root1");
});
test("Reading an unread thread after a redaction of the latest message makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread thread where the latest message was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertUnreadThread("Root");
// When I read the thread
await util.openThread("Root");
await util.assertRead(room2);
await util.closeThreadsPanel();
await util.goTo(room1);
// Then the thread is read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
});
test("Reading an unread thread after a redaction of the latest message makes it read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message is not counted in the unread count
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertUnreadThread("Root");
await util.openThread("Root");
await util.assertRead(room2);
await util.closeThreadsPanel();
await util.goTo(room1);
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
// When I restart
await util.saveAndReload();
// Then the room and thread are still read
await util.assertRead(room2);
await util.openThreadList();
await util.assertReadThread("Root");
});
test("Reading an unread thread after a redaction of an older message makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread thread where an older message was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertUnreadThread("Root");
// When I read the thread
await util.openThread("Root");
await util.assertRead(room2);
await util.closeThreadsPanel();
await util.goTo(room1);
// Then the thread is read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
});
test("Marking an unread thread as read after a redaction makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread thread where an older message was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]);
await util.assertUnread(room2, 1);
// When I mark the room as read
await util.markAsRead(room2);
await util.assertRead(room2);
// Then the thread is read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
});
test("Sending and redacting a message after marking the thread as read leaves it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists and is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I send and redact a message
await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg3")]);
await util.goTo(room2);
await util.openThreadList();
await util.assertUnreadThread("Root");
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// Then the room and thread are read
await util.goTo(room2);
await util.assertReadThread("Root");
});
test("Redacting a message after marking the thread as read leaves it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists and is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I redact a message
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]);
// Then the room and thread are read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
});
test("Reacting to a redacted message leaves the thread read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message in a thread was redacted and everything is read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.backToThreadsList();
await util.assertReadThread("Root");
await util.goTo(room1);
// When we receive a reaction to the redacted event
await util.receiveMessages(room2, [msg.reactionTo("Msg2", "z")]);
// Then the room is read
await util.assertStillRead(room2);
await util.goTo(room2);
await util.openThreadList();
await util.assertReadThread("Root");
});
test("Editing a redacted message leaves the thread read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message in a thread was redacted and everything is read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
await util.openThreadList();
await util.assertUnreadThread("Root");
await util.openThread("Root");
await util.assertReadThread("Root");
await util.goTo(room1);
// When we receive an edit of the redacted message
await util.receiveMessages(room2, [msg.editOf("Msg2", "New Msg2")]);
// Then the room is unread
await util.assertStillRead(room2);
// and so is the thread
await util.goTo(room2);
await util.openThreadList();
await util.assertReadThread("Root");
});
test("Reading a thread after a reaction to a redacted message marks the thread as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message in a thread exists, but someone reacted to it before it was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
msg.reactionTo("Msg3", "x"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
await util.assertUnread(room2, 1);
// When we read the thread
await util.goTo(room2);
await util.openThread("Root");
// Then the thread (and room) are read
await util.assertRead(room2);
await util.assertReadThread("Root");
});
test("Reading a thread containing a redacted, edited message marks the thread as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message in a thread exists, but someone edited it before it was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
msg.editOf("Msg3", "Msg3 Edited"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// When we read the thread
await util.goTo(room2);
await util.openThread("Root");
// Then the thread (and room) are read
await util.assertRead(room2);
await util.assertReadThread("Root");
});
test("Reading a reply to a redacted message marks the thread as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message in a thread exists, but someone replied before it was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
msg.replyTo("Msg3", "Msg3Reply"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// When we read the thread, creating a receipt that points at the edit
await util.goTo(room2);
await util.openThread("Root");
// Then the thread (and room) are read
await util.assertRead(room2);
await util.assertReadThread("Root");
});
test("Reading a thread root when its only message has been redacted leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given we had a thread
await util.goTo(room1);
await util.receiveMessages(room2, ["Root", msg.threadedOff("Root", "Msg2")]);
await util.assertUnread(room2, 1);
// And then redacted the message that makes it a thread
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
// When we read the main timeline
await util.goTo(room2);
// Then the room is read
await util.assertRead(room2);
// and that thread is read
await util.openThreadList();
await util.assertReadThread("Root");
});
test("A thread with a redacted unread is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I sent and redacted a message in an otherwise-read thread
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "ThreadMsg1"),
msg.threadedOff("Root", "ThreadMsg2"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg3")]);
await util.assertRead(room2);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
await util.goTo(room1);
// When I restart
await util.saveAndReload();
// Then the room and thread are still read
await util.assertRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root");
});
/*
* Disabled: this doesn't seem to work as, at some point after syncing from cache, the redaction and redacted
* event get removed from the thread timeline such that we have no record of the events that the read receipt
* points to. I suspect this may have been passing by fluke before.
*/
test.skip("A thread with a read redaction is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given my receipt points at a redacted thread message
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root1",
msg.threadedOff("Root1", "ThreadMsg1"),
msg.threadedOff("Root1", "ThreadMsg2"),
"Root2",
msg.threadedOff("Root2", "Root2->A"),
]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.assertUnreadThread("Root1");
await util.openThread("Root1");
await util.assertRead(room2);
await util.openThread("Root2");
await util.assertRead(room2);
await util.closeThreadsPanel();
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
await util.assertStillRead(room2);
await util.goTo(room2);
await util.assertReadThread("Root1");
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
// and so is the thread
await util.openThreadList();
await util.assertReadThread("Root1");
await util.assertReadThread("Root2");
});
test("A thread with an unread reply to a redacted message is still unread after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message in a thread exists, but someone replied before it was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
msg.replyTo("Msg3", "Msg3Reply"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// And we have read all this
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
await util.assertReadThread("Root");
});
test("A thread with a read reply to a redacted message is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message in a thread exists, but someone replied before it was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
msg.replyTo("Msg3", "Msg3Reply"),
]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// And I read it, so the room is read
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
await util.assertReadThread("Root");
});
});
});
});
@@ -0,0 +1,326 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("redactions", () => {
test.describe("in the main timeline", () => {
test("Redacting the message pointed to by my receipt leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given I have read the messages in a room
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When the latest message is redacted
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
// Then the room remains read
await util.assertStillRead(room2);
});
test("Reading an unread room after a redaction of the latest message makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread room
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
// And the latest message has been redacted
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
// When I read the room
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// Then it becomes read
await util.assertStillRead(room2);
});
test("Reading an unread room after a redaction of an older message makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread room with an earlier redaction
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg1")]);
// When I read the room
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// Then it becomes read
await util.assertStillRead(room2);
});
test("Marking an unread room as read after a redaction makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread room where latest message is redacted
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
// When I mark it as read
await util.markAsRead(room2);
// Then it becomes read
await util.assertRead(room2);
});
test("Sending and redacting a message after marking the room as read makes it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a room that is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
// When a message is sent and then redacted
await util.receiveMessages(room2, ["Msg3"]);
await util.assertUnread(room2, 1);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// Then the room is read
await util.assertRead(room2);
});
test("Redacting a message after marking the room as read leaves it read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a room that is marked as read
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]);
await util.assertUnread(room2, 3);
await util.markAsRead(room2);
await util.assertRead(room2);
// When we redact some messages
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
await util.receiveMessages(room2, [msg.redactionOf("Msg1")]);
// Then it is still read
await util.assertStillRead(room2);
});
test("Redacting one of the unread messages reduces the unread count", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread room
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]);
await util.assertUnread(room2, 3);
// When I redact a non-latest message
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
// Then the unread count goes down
await util.assertUnread(room2, 2);
// And when I redact the latest message
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
// Then the unread count goes down again
await util.assertUnread(room2, 1);
});
test("Redacting one of the unread messages reduces the unread count after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given unread count was reduced by redacting messages
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]);
await util.assertUnread(room2, 3);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
await util.assertUnread(room2, 1);
// When I restart
await util.saveAndReload();
// Then the unread count is still reduced
await util.assertUnread(room2, 1);
});
test("Redacting all unread messages makes the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread room
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
// When I redact all the unread messages
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.receiveMessages(room2, [msg.redactionOf("Msg1")]);
// Then the room is back to being read
await util.assertRead(room2);
});
test("Redacting all unread messages makes the room read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given all unread messages were redacted
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.receiveMessages(room2, [msg.redactionOf("Msg1")]);
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
});
test("Reacting to a redacted message leaves the room read", async ({
page,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message exists
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
// And the room is read
await util.goTo(room2);
await util.assertRead(room2);
await page.waitForTimeout(200);
await util.goTo(room1);
// When I react to the redacted message
await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]);
// Then the room is still read
await util.assertStillRead(room2);
});
test("Editing a redacted message leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a redacted message exists
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
// And the room is read
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When I attempt to edit the redacted message
await util.receiveMessages(room2, [msg.editOf("Msg2", "Msg2 is BACK")]);
// Then the room is still read
await util.assertStillRead(room2);
});
test("A reply to a redacted message makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a message was redacted
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
// And the room is read
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
// When I receive a reply to the redacted message
await util.receiveMessages(room2, [msg.replyTo("Msg2", "Reply to Msg2")]);
// Then the room is unread
await util.assertUnread(room2, 1);
});
test("Reading a reply to a redacted message marks the room as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given someone replied to a redacted message
await util.goTo(room1);
await util.receiveMessages(room2, ["Msg1", "Msg2"]);
await util.assertUnread(room2, 2);
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.assertRead(room2);
await util.goTo(room1);
await util.receiveMessages(room2, [msg.replyTo("Msg2", "Reply to Msg2")]);
await util.assertUnread(room2, 1);
// When I read the reply
await util.goTo(room2);
await util.assertRead(room2);
// Then the room is unread
await util.goTo(room1);
await util.assertStillRead(room2);
});
});
});
});
@@ -0,0 +1,227 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
test.describe("redactions", () => {
test.describe("thread roots", () => {
test("Redacting a thread root after it was read leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
test.slow();
// Given a thread exists and is read
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
// When someone redacts the thread root
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
// Then the room is still read
await util.assertStillRead(room2);
});
/*
* Disabled for the same reason as "A thread with a read redaction is still read after restart"
* above
*/
test.skip("Redacting a thread root still allows us to read the thread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given an unread thread exists
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
// When someone redacts the thread root
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
// Then the room is still unread
await util.assertUnread(room2, 1);
// And I can open the thread and read it
await util.goTo(room2);
await util.assertRead(room2);
// The redacted message gets collapsed into, "foo was invited, joined and removed a message"
await util.openCollapsedMessage(1);
await util.openThread("Message deleted");
await util.assertRead(room2);
await util.assertReadThread("Root");
});
test("Sending a threaded message onto a redacted thread root leaves the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists, is read and its root is redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
// When we receive a new message on it
await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg4")]);
// Then the room is read but the thread is unread
await util.assertRead(room2);
await util.goTo(room2);
await util.assertUnreadThread("Message deleted");
});
test("Reacting to a redacted thread root leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists, is read and the root was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
// When I react to the old root
await util.receiveMessages(room2, [msg.reactionTo("Root", "y")]);
// Then the room is still read
await util.assertRead(room2);
await util.assertReadThread("Root");
});
test("Editing a redacted thread root leaves the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists, is read and the root was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
// When I edit the old root
await util.receiveMessages(room2, [msg.editOf("Root", "New Root")]);
// Then the room is still read
await util.assertRead(room2);
// as is the thread
await util.assertReadThread("Root");
});
test("Replying to a redacted thread root makes the room unread", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists, is read and the root was redacted
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
// When I reply to the old root
await util.receiveMessages(room2, [msg.replyTo("Root", "Reply!")]);
// Then the room is unread
await util.assertUnread(room2, 1);
});
test("Reading a reply to a redacted thread root makes the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given a thread exists, is read and the root was redacted, and
// someone replied to it
await util.goTo(room1);
await util.receiveMessages(room2, [
"Root",
msg.threadedOff("Root", "Msg2"),
msg.threadedOff("Root", "Msg3"),
]);
await util.assertUnread(room2, 1);
await util.goTo(room2);
await util.openThread("Root");
await util.assertRead(room2);
await util.assertReadThread("Root");
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
await util.assertStillRead(room2);
await util.receiveMessages(room2, [msg.replyTo("Root", "Reply!")]);
await util.assertUnread(room2, 1);
// When I read the room
await util.goTo(room2);
// Then it becomes read
await util.assertRead(room2);
});
});
});
});
@@ -0,0 +1,75 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Room list order", () => {
test("Rooms with unread messages appear at the top of room list with default 'activity' ordering", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
page,
app,
bot,
}) => {
// Create a third room to navigate to
const room3Id = await app.client.createRoom({ name: "Room Gamma", invite: [bot.credentials.userId] });
await bot.awaitRoomMembership(room3Id);
const room3 = { name: "Room Gamma", roomId: room3Id };
await util.goTo(room2);
// Display the unread first room
await util.receiveMessages(room2, ["Msg2"]);
await util.receiveMessages(room1, ["Msg1"]);
await page.reload();
// Switch to room3 so neither room1 nor room2 is selected/sticky
// This allows them to reorder based on activity
await util.goTo(room3);
// Room 1 has an unread message and should be displayed first
// (as the default is to sort by activity)
await util.assertRoomListOrder([room1, room2, room3]);
});
test("Rooms with unread threads appear at the top of room list with default 'activity' order", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
app,
bot,
}) => {
// Create a third room to navigate to
const room3Id = await app.client.createRoom({ name: "Room Gamma", invite: [bot.credentials.userId] });
await bot.awaitRoomMembership(room3Id);
const room3 = { name: "Room Gamma", roomId: room3Id };
await util.goTo(room2);
await util.receiveMessages(room1, ["Msg1"]);
await util.receiveMessages(room2, ["Msg2"]);
await util.markAsRead(room1);
await util.assertRead(room1);
// Display the unread first room (room1 moves above room2 as it has an unread thread)
await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]);
await util.saveAndReload();
// Switch to room3 so neither room1 nor room2 is selected/sticky
await util.goTo(room3);
// Room 1 has an unread message and should be displayed first
await util.assertRoomListOrder([room1, room2, room3]);
});
});
});