Migrate most read-receipts tests from Cypress to Playwright (#11994)

* Migrate most read-receipts tests from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Disable failing test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2023-12-05 10:37:23 +00:00 committed by GitHub
parent 236ea44cc8
commit b691be3bee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 3603 additions and 3319 deletions

View file

@ -0,0 +1,492 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
test.describe("Read receipts", () => {
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);
});
// XXX: fails because flaky: https://github.com/vector-im/element-web/issues/26341
test.skip("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(room2);
await util.receiveMessages(room2, ["Msg1"]);
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);
// 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);
});
});
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, 2);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.backToThreadsList();
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, 2);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.backToThreadsList();
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, 2);
// When I mark the room as read
await util.markAsRead(room2);
// Then it is read
await util.assertRead(room2);
});
// XXX: flaky
test.skip("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, 2);
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);
});
// XXX: flaky
test.skip("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);
});
// XXX: Failing since migration to Playwright
test.skip("A room where all threaded edits are read is still read after restart", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
await util.goTo(room2);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
msg.editOf("Resp1", "Edit1"),
]);
await util.assertUnread(room2, 1);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1); // Make sure we are looking at room1 after reload
await util.assertStillRead(room2);
await util.saveAndReload();
await util.assertRead(room2);
});
// XXX: fails because the room becomes unread after restart
test.skip("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, 2);
await util.markAsRead(room2);
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// It is still read
await util.assertRead(room2);
});
});
test.describe("thread roots", () => {
// XXX: flaky
test.skip("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, 2);
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);
});
// XXX: flaky
test.skip("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, 3);
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, 3);
// 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");
});
});
});
});

View file

@ -0,0 +1,466 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* See readme.md for tips on writing these tests. */
import { customEvent, many, test } from ".";
test.describe("Read receipts", () => {
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",
() => {},
);
});
});
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", () => {
// XXX: Fails because flaky test https://github.com/vector-im/element-web/issues/26437
test.skip("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.name, "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.assertUnread(room2, 60);
await util.openThread("Root1");
await util.assertUnread(room2, 40);
await util.assertReadThread("Root1");
await util.openThread("Root2");
await util.assertUnread(room2, 20);
await util.assertReadThread("Root2");
await util.openThread("Root3");
await util.assertRead(room2);
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 ({
cryptoBackend,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
test.slow();
test.skip(
cryptoBackend === "rust",
"Flaky with rust crypto - see https://github.com/vector-im/element-web/issues/26539",
);
// 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.assertUnread(room2, 6);
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 unread
// TODO: I (andyb) think this will fall in an encrypted room
await util.assertUnread(room2, 6);
// And when I page up to load old thread roots
await util.goTo(room2);
await util.pageUp();
// Then the room remains unread
await util.assertUnread(room2, 6);
await util.assertUnreadThread("Root1");
await util.assertUnreadThread("Root2");
await util.assertUnreadThread("Root3");
});
// XXX: fails because flaky: https://github.com/vector-im/element-web/issues/26331
test.skip("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.assertUnread(room2, 6);
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 unread
// TODO: I (andyb) think this will fall in an encrypted room
await util.assertUnread(room2, 6);
// And when I open the threads view
await util.goTo(room2);
await util.openThreadList();
// Then the room remains unread
await util.assertUnread(room2, 6);
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 ({
cryptoBackend,
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
test.slow();
test.skip(
cryptoBackend === "rust",
"Flaky with rust crypto - see https://github.com/vector-im/element-web/issues/26341",
);
// 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");
});
// XXX: fails because we see a dot instead of an unread number - probably the server and client disagree
test.skip("After marking room as read based on a thread message, opening threads view to find old threads that were never read leaves the room read", 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");
});
});
test.describe("Room list order", () => {
test.fixme("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {});
});
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", () => {});
});
});
});

View file

@ -0,0 +1,591 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { JSHandle, 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 { Bot } from "../../pages/bot";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
/**
* 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: { name: string; roomId: string };
roomBetaName?: string;
roomBeta: { name: string; roomId: string };
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 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 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) => {
return {
"msgtype": "m.text",
"body": newMessage,
"m.relates_to": {
"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.sendEvent(roomId, threadId ?? null, "m.reaction", {
"m.relates_to": {
rel_type: "m.annotation",
event_id: id,
key: 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 roomName the name 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(roomName: string, message: string, includeThreads = false) {
const room = await this.helpers.findRoomByName(roomName);
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, roomName: string | { name: string }, messages: Message[]) {
const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name);
const roomId = await room.evaluate((room) => room.roomId);
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: string | { name: string }) {
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
}
/**
* 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. (Actually, close any right panel, but for these
* tests we only open the threads panel.)
*/
async closeThreadsPanel() {
await this.page.locator(".mx_RightPanel").getByTitle("Close").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_RightPanel").getByTitle("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(room: string | { name: string }) {
const roomName = typeof room === "string" ? room : room.name;
return this.page.getByRole("treeitem", { name: new RegExp("^" + roomName) });
}
/**
* Click the "Mark as Read" context menu item on the room with the supplied name
* in the room list.
*/
async markAsRead(room: string | { name: string }) {
await this.getRoomListTile(room).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: string | { name: string }) {
const tile = this.getRoomListTile(room);
await expect(tile.locator(".mx_NotificationBadge_dot")).not.toBeVisible();
await expect(tile.locator(".mx_NotificationBadge_count")).not.toBeVisible();
}
/**
* 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: string | { name: string }) {
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, or if "." specified then a bold/dot (no count) state is asserted
*/
async assertUnread(room: string | { name: string }, count: number | ".") {
const tile = this.getRoomListTile(room);
if (count === ".") {
await expect(tile.locator(".mx_NotificationBadge_dot")).toBeVisible();
} else {
await expect(tile.locator(".mx_NotificationBadge_count")).toHaveText(count.toString());
}
}
/**
* 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: string | { name: string }, lessThan: number) {
const tile = this.getRoomListTile(room);
expect(parseInt(await tile.locator(".mx_NotificationBadge_count").textContent(), 10)).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: string | { name: string }, greaterThan: number) {
const tile = this.getRoomListTile(room);
expect(parseInt(await tile.locator(".mx_NotificationBadge_count").textContent(), 10)).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 ariaCurrent = await this.page.getByTestId("threadsButton").getAttribute("aria-current");
if (ariaCurrent !== "true") {
await this.page.getByTestId("threadsButton").click();
}
const threadPanel = this.page.locator(".mx_ThreadPanel");
await expect(threadPanel).toBeVisible();
await threadPanel.evaluate(($panel) => {
const $button = $panel.querySelector<HTMLElement>('.mx_BaseCard_back[title="Threads"]');
// If the Threads back button is present then click it - the
// threads button can open either threads list or thread panel
if ($button) {
$button.click();
}
});
}
async findRoomByName(roomName: string): Promise<JSHandle<Room>> {
return this.app.client.evaluateHandle((cli, roomName) => {
return cli.getRooms().find((r) => r.name === roomName);
}, roomName);
}
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: string | { name: string }, messages: Message[]) {
await this.sendMessageAsClient(this.bot, room, messages);
}
}
/**
* 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 };

View file

@ -0,0 +1,59 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* See readme.md for tips on writing these tests. */
import { test } from ".";
test.describe("Read receipts", () => {
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?
});
});

View file

@ -0,0 +1,574 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* See readme.md for tips on writing these tests. */
import { many, test } from ".";
test.describe("Read receipts", () => {
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);
});
// XXX: fails (sometimes!) because the unread count stays high
test.skip("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.name, "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);
});
// XXX: fails because the room remains unread even though I sent a message
// Note: this test should not re-use the same MatrixClient - it
// should create a new one logged in as the same user.
test.skip("Me sending a message from a different client marks room as read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
app,
}) => {
// Given I have unread messages
await util.goTo(room1);
await util.assertRead(room2);
await util.receiveMessages(room2, ["Msg1"]);
await util.assertUnread(room2, 1);
// When I send a new message from a different client
await util.sendMessageAsClient(app.client, room2, ["Msg2"]);
// Then this room is marked as read
await util.assertRead(room2);
});
});
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 becomes unread
await util.assertUnread(room2, 1);
});
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, 2);
await util.goTo(room2);
// When I read it
await util.openThread("Msg1");
// The room becomes read
await util.assertRead(room2);
});
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, 3); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
// Then room does appear unread
await util.assertUnread(room2, 2);
// Until we open the thread
await util.openThread("Msg1");
await util.assertReadThread("Msg1");
await util.assertRead(room2);
});
// XXX: Fails since migration to Playwright
test.skip("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, 21);
// When I read an older message in the thread
await msg.jumpTo(room2.name, "InThread0001", true);
await util.assertUnreadLessThan(room2, 21);
// TODO: for some reason, we can't find the first message
// "InThread0", so I am using the second here. Also, they appear
// out of order, with "InThread2" before "InThread1". Might be a
// clue to the sporadic reports we have had of messages going
// missing in threads?
// Then the thread is still marked as unread
await util.backToThreadsList();
await util.assertUnreadThread("ThreadRoot");
});
test("Reading only one thread's message does not make the room read", async ({
roomAlpha: room1,
roomBeta: room2,
util,
msg,
}) => {
// Given two threads are unread
await util.goTo(room1);
await util.receiveMessages(room2, [
"Msg1",
msg.threadedOff("Msg1", "Resp1"),
"Msg2",
msg.threadedOff("Msg2", "Resp2"),
]);
await util.assertUnread(room2, 4);
await util.goTo(room2);
await util.assertUnread(room2, 2);
// When I only read one of them
await util.openThread("Msg1");
// The room is still unread
await util.assertUnread(room2, 1);
});
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, 4); // (Sanity)
await util.goTo(room2);
await util.assertUnread(room2, 2);
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, 3); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
await util.assertUnread(room2, 2);
// 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, 3); // (Sanity)
// When I mark the room as read
await util.markAsRead(room2);
// Then the room is read
await util.assertRead(room2);
});
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 room becomes unread
await util.assertUnread(room2, 1);
});
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.assertUnread(room2, 3);
await util.receiveMessages(room2, [msg.threadedOff("Thread2", "t2a")]);
// Make sure the 4th message has arrived before we mark as read.
await util.assertUnread(room2, 4);
// 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 room becomes unread
await util.assertUnread(room2, 1);
});
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, 3); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
// Then room does appear unread
await util.assertUnread(room2, 2);
await util.saveAndReload();
await util.assertUnread(room2, 2);
// Until we open the thread
await util.openThread("Msg1");
await util.assertRead(room2);
});
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, 3); // (Sanity)
await util.goTo(room2);
await util.assertUnread(room2, 2);
await util.openThread("Msg1");
await util.assertRead(room2);
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
});
});
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, 2); // (Sanity)
// When I read the main timeline
await util.goTo(room2);
// Then room does appear unread
await util.assertUnread(room2, 1);
await util.assertUnreadThread("Msg1");
});
// XXX: fails because we jump to the wrong place in the timeline
test.skip("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, 62); // Sanity
// When I jump to an old message and read the thread
await msg.jumpTo(room2.name, "beforeThread0000");
await util.openThread("ThreadRoot");
// Then the thread root is marked as read in the main timeline,
// so there are only 30 left - the ones after the thread root.
await util.assertUnread(room2, 30);
});
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 room is unread
await util.assertUnread(room2, 1);
});
test("Reading a thread whose root is a reply makes the room 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, 3);
await util.goTo(room2);
await util.assertUnread(room2, 1);
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");
});
});
});
});

View file

@ -0,0 +1,359 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* See readme.md for tips on writing these tests. */
import { test, expect } from ".";
test.describe("Read receipts", () => {
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);
});
});
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, 2);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
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);
});
// XXX: fails because the room is still "bold" even though the notification counts all disappear
test.skip("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, 2);
// When I mark the room as read
await util.markAsRead(room2);
// Then it becomes read
await util.assertRead(room2);
});
// XXX: fails because the room is still "bold" even though the notification counts all disappear
test.skip("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"),
msg.reactionTo("Reply1", "🪿"),
]);
await util.assertUnread(room2, 2);
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);
});
test.skip("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, 2);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
await util.goTo(room1);
// And someone reacted to it, which doesn't stop it being read
await util.receiveMessages(room2, [msg.reactionTo("Reply1", "🪿")]);
await util.assertStillRead(room2);
// When I restart
await util.saveAndReload();
// Then the room is still read
await util.assertRead(room2);
});
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, 6);
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, 2);
// 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();
});
});
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, 2);
await util.goTo(room2);
await util.openThread("Msg1");
await util.assertRead(room2);
// 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);
});
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, 2);
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);
// 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);
});
// XXX: fails because the room is still "bold" even though the notification counts all disappear
test.skip("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, 2);
// And we have marked the room as read
await util.markAsRead(room2);
await util.assertRead(room2);
// 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);
});
});
});
});

View file

@ -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.

File diff suppressed because it is too large Load diff

View file

@ -52,6 +52,19 @@ export class Client {
return this.client.evaluate(fn, arg);
}
public evaluateHandle<R, Arg, O extends MatrixClient = MatrixClient>(
pageFunction: PageFunctionOn<O, Arg, R>,
arg: Arg,
): Promise<JSHandle<R>>;
public evaluateHandle<R, O extends MatrixClient = MatrixClient>(
pageFunction: PageFunctionOn<O, void, R>,
arg?: any,
): Promise<JSHandle<R>>;
public async evaluateHandle<T>(fn: (client: MatrixClient) => T, arg?: any): Promise<JSHandle<T>> {
await this.prepareClient();
return this.client.evaluateHandle(fn, arg);
}
/**
* @param roomId ID of the room to send the event into
* @param threadId ID of the thread to send into or null for main timeline
@ -91,6 +104,15 @@ export class Client {
);
}
public async redactEvent(roomId: string, eventId: string, reason?: string): Promise<ISendEventResponse> {
return this.evaluate(
async (client, { roomId, eventId, reason }) => {
return client.redactEvent(roomId, eventId, reason);
},
{ roomId, eventId, reason },
);
}
/**
* Create a room with given options.
* @param options the options to apply when creating the room