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:
parent
236ea44cc8
commit
b691be3bee
16 changed files with 3603 additions and 3319 deletions
|
@ -1,452 +0,0 @@
|
||||||
/*
|
|
||||||
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. */
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import {
|
|
||||||
assertRead,
|
|
||||||
assertReadThread,
|
|
||||||
assertStillRead,
|
|
||||||
assertUnread,
|
|
||||||
backToThreadsList,
|
|
||||||
goTo,
|
|
||||||
markAsRead,
|
|
||||||
Message,
|
|
||||||
MessageContentSpec,
|
|
||||||
MessageFinder,
|
|
||||||
openThread,
|
|
||||||
ReadReceiptSetup,
|
|
||||||
saveAndReload,
|
|
||||||
sendMessageAsClient,
|
|
||||||
} from "./read-receipts-utils";
|
|
||||||
|
|
||||||
describe("Read receipts", () => {
|
|
||||||
const roomAlpha = "Room Alpha";
|
|
||||||
const roomBeta = "Room Beta";
|
|
||||||
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
let messageFinder: MessageFinder;
|
|
||||||
let testSetup: ReadReceiptSetup;
|
|
||||||
|
|
||||||
function editOf(originalMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.editOf(originalMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function replyTo(targetMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.replyTo(targetMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.threadedOff(rootMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Note: unusually for the Cypress tests in this repo, we share a single
|
|
||||||
// Synapse between all the tests in this file.
|
|
||||||
//
|
|
||||||
// Stopping and starting Synapse costs about 0.25 seconds per test, so
|
|
||||||
// for most suites this is worth the cost for the extra assurance that
|
|
||||||
// each test is independent.
|
|
||||||
//
|
|
||||||
// Because there are so many tests in this file, and because sharing a
|
|
||||||
// Synapse should have no effect (because we create new rooms and users
|
|
||||||
// for each test), we share it here, saving ~30 seconds per run at time
|
|
||||||
// of writing.
|
|
||||||
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messageFinder = new MessageFinder();
|
|
||||||
testSetup = new ReadReceiptSetup(homeserver, "Mae", "Other User", roomAlpha, roomBeta);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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`
|
|
||||||
*/
|
|
||||||
function receiveMessages(room: string, messages: Message[]) {
|
|
||||||
sendMessageAsClient(testSetup.bot, room, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const room1 = roomAlpha;
|
|
||||||
const room2 = roomBeta;
|
|
||||||
|
|
||||||
describe("editing messages", () => {
|
|
||||||
describe("in the main timeline", () => {
|
|
||||||
it("Editing a message leaves a room read", () => {
|
|
||||||
// Given I am not looking at the room
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When an edit appears in the room
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
|
|
||||||
// Then it remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Reading an edit leaves the room read", () => {
|
|
||||||
// Given an edit is making the room unread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// When I read it
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then the room stays read
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Editing a message after marking as read leaves the room read", () => {
|
|
||||||
// Given the room is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When a message is edited
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Editing a reply after reading it makes the room unread", () => {
|
|
||||||
// Given the room is all read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When a message is edited
|
|
||||||
receiveMessages(room2, [editOf("Reply1", "Reply1 Edit1")]);
|
|
||||||
|
|
||||||
// Then it remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Editing a reply after marking as read makes the room unread", () => {
|
|
||||||
// Given a reply is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When the reply is edited
|
|
||||||
receiveMessages(room2, [editOf("Reply1", "Reply1 Edit1")]);
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because flaky: https://github.com/vector-im/element-web/issues/26341
|
|
||||||
it.skip("A room with an edit is still read after restart", () => {
|
|
||||||
// Given a message is marked as read
|
|
||||||
goTo(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When an edit appears in the room
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
|
|
||||||
// Then it remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// And remains so after a reload
|
|
||||||
saveAndReload();
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("An edited message becomes read if it happens while I am looking", () => {
|
|
||||||
// Given a message is marked as read
|
|
||||||
goTo(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I see an edit appear in the room I am looking at
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
|
|
||||||
// Then it becomes read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("A room where all edits are read is still read after restart", () => {
|
|
||||||
// Given a message was edited and read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I reload
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("in threads", () => {
|
|
||||||
it("An edit of a threaded message makes the room unread", () => {
|
|
||||||
// Given we have read the thread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
backToThreadsList();
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When a message inside it is edited
|
|
||||||
receiveMessages(room2, [editOf("Resp1", "Edit1")]);
|
|
||||||
|
|
||||||
// Then the room and thread are read
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Msg1");
|
|
||||||
});
|
|
||||||
it("Reading an edit of a threaded message makes the room read", () => {
|
|
||||||
// Given an edited thread message appears after we read it
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
backToThreadsList();
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [editOf("Resp1", "Edit1")]);
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// When I read it
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
|
|
||||||
// Then the room and thread are still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
assertReadThread("Msg1");
|
|
||||||
});
|
|
||||||
it("Marking a room as read after an edit in a thread makes it read", () => {
|
|
||||||
// Given an edit in a thread is making the room unread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then it is read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: flaky
|
|
||||||
it.skip("Editing a thread message after marking as read leaves the room read", () => {
|
|
||||||
// Given a room is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When a message is edited
|
|
||||||
receiveMessages(room2, [editOf("Resp1", "Edit1")]);
|
|
||||||
|
|
||||||
// Then the room becomes unread
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: flaky
|
|
||||||
it.skip("A room with an edited threaded message is still read after restart", () => {
|
|
||||||
// Given an edit in a thread is leaving a room read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
markAsRead(room2);
|
|
||||||
receiveMessages(room2, [editOf("Resp1", "Edit1")]);
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then is it still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("A room where all threaded edits are read is still read after restart", () => {
|
|
||||||
goTo(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1); // Make sure we are looking at room1 after reload
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
saveAndReload();
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because the room becomes unread after restart
|
|
||||||
it.skip("A room where all threaded edits are marked as read is still read after restart", () => {
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// It is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("thread roots", () => {
|
|
||||||
// XXX: flaky
|
|
||||||
it.skip("An edit of a thread root leaves the room read", () => {
|
|
||||||
// Given I have read a thread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
backToThreadsList();
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When the thread root is edited
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Edit1")]);
|
|
||||||
|
|
||||||
// Then the room is read
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// And the thread is read
|
|
||||||
goTo(room2);
|
|
||||||
assertStillRead(room2);
|
|
||||||
assertReadThread("Edit1");
|
|
||||||
});
|
|
||||||
it("Reading an edit of a thread root leaves the room read", () => {
|
|
||||||
// Given a fully-read thread exists
|
|
||||||
goTo(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When the thread root is edited
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
|
|
||||||
// And I read that edit
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then the room becomes read and stays read
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Editing a thread root after reading leaves the room read", () => {
|
|
||||||
// Given a fully-read thread exists
|
|
||||||
goTo(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When the thread root is edited
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]);
|
|
||||||
|
|
||||||
// Then the room stays read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Marking a room as read after an edit of a thread root keeps it read", () => {
|
|
||||||
// Given a fully-read thread exists
|
|
||||||
goTo(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When the thread root is edited (and I receive another message
|
|
||||||
// to allow Mark as read)
|
|
||||||
receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1"), "Msg2"]);
|
|
||||||
|
|
||||||
// And when I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then the room becomes read and stays read
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: flaky
|
|
||||||
it.skip("Editing a thread root that is a reply after marking as read leaves the room read", () => {
|
|
||||||
// Given a thread based on a reply exists and is read because it is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg", replyTo("Msg", "Reply"), threadedOff("Reply", "InThread")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I edit the thread root
|
|
||||||
receiveMessages(room2, [editOf("Reply", "Edited Reply")]);
|
|
||||||
|
|
||||||
// Then the room is read
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// And the thread is read
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Edited Reply");
|
|
||||||
});
|
|
||||||
it("Marking a room as read after an edit of a thread root that is a reply leaves it read", () => {
|
|
||||||
// Given a thread based on a reply exists and the reply has been edited
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg", replyTo("Msg", "Reply"), threadedOff("Reply", "InThread")]);
|
|
||||||
receiveMessages(room2, [editOf("Reply", "Edited Reply")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then the room and thread are read
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Edited Reply");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,439 +0,0 @@
|
||||||
/*
|
|
||||||
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. */
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import {
|
|
||||||
assertMessageLoaded,
|
|
||||||
assertMessageNotLoaded,
|
|
||||||
assertRead,
|
|
||||||
assertReadThread,
|
|
||||||
assertStillRead,
|
|
||||||
assertUnread,
|
|
||||||
assertUnreadGreaterThan,
|
|
||||||
assertUnreadThread,
|
|
||||||
closeThreadsPanel,
|
|
||||||
customEvent,
|
|
||||||
goTo,
|
|
||||||
many,
|
|
||||||
markAsRead,
|
|
||||||
Message,
|
|
||||||
MessageContentSpec,
|
|
||||||
MessageFinder,
|
|
||||||
openThread,
|
|
||||||
openThreadList,
|
|
||||||
pageUp,
|
|
||||||
ReadReceiptSetup,
|
|
||||||
saveAndReload,
|
|
||||||
sendMessageAsClient,
|
|
||||||
} from "./read-receipts-utils";
|
|
||||||
import { skipIfRustCrypto } from "../../support/util";
|
|
||||||
|
|
||||||
describe("Read receipts", () => {
|
|
||||||
const roomAlpha = "Room Alpha";
|
|
||||||
const roomBeta = "Room Beta";
|
|
||||||
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
let messageFinder: MessageFinder;
|
|
||||||
let testSetup: ReadReceiptSetup;
|
|
||||||
|
|
||||||
function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.threadedOff(rootMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function manyThreadedOff(rootMessage: string, newMessages: Array<string>): Array<MessageContentSpec> {
|
|
||||||
return messageFinder.manyThreadedOff(rootMessage, newMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
function jumpTo(room: string, message: string, includeThreads = false) {
|
|
||||||
return messageFinder.jumpTo(room, message, includeThreads);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Note: unusually for the Cypress tests in this repo, we share a single
|
|
||||||
// Synapse between all the tests in this file.
|
|
||||||
//
|
|
||||||
// Stopping and starting Synapse costs about 0.25 seconds per test, so
|
|
||||||
// for most suites this is worth the cost for the extra assurance that
|
|
||||||
// each test is independent.
|
|
||||||
//
|
|
||||||
// Because there are so many tests in this file, and because sharing a
|
|
||||||
// Synapse should have no effect (because we create new rooms and users
|
|
||||||
// for each test), we share it here, saving ~30 seconds per run at time
|
|
||||||
// of writing.
|
|
||||||
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messageFinder = new MessageFinder();
|
|
||||||
testSetup = new ReadReceiptSetup(homeserver, "Mae", "Other User", roomAlpha, roomBeta);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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`
|
|
||||||
*/
|
|
||||||
function receiveMessages(room: string, messages: Message[]) {
|
|
||||||
sendMessageAsClient(testSetup.bot, room, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const room1 = roomAlpha;
|
|
||||||
const room2 = roomBeta;
|
|
||||||
|
|
||||||
describe("Message ordering", () => {
|
|
||||||
describe("in the main timeline", () => {
|
|
||||||
it.skip("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {});
|
|
||||||
it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("in threads", () => {
|
|
||||||
// These don't pass yet - we need MSC4033 - we don't even know the Sync order yet
|
|
||||||
it.skip("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {});
|
|
||||||
it.skip("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {});
|
|
||||||
|
|
||||||
// These pass now and should not later - we should use order from MSC4033 instead of ts
|
|
||||||
// These are broken out
|
|
||||||
it.skip("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {});
|
|
||||||
it.skip("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {});
|
|
||||||
it.skip("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {});
|
|
||||||
it.skip("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {});
|
|
||||||
it.skip("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {});
|
|
||||||
it.skip("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("thread roots", () => {
|
|
||||||
it.skip("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {});
|
|
||||||
it.skip("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {});
|
|
||||||
it.skip("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {});
|
|
||||||
it.skip("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Ignored events", () => {
|
|
||||||
it("If all events after receipt are unimportant, the room is read", () => {
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]);
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Sending an important event after unimportant ones makes the room unread", () => {
|
|
||||||
// Given We have read the important messages
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When we receive unimportant messages
|
|
||||||
receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// And when we receive more important ones
|
|
||||||
receiveMessages(room2, ["Hello"]);
|
|
||||||
|
|
||||||
// The room is unread again
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {
|
|
||||||
// Display room 1
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// The room 2 is read
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// We received 3 unimportant messages to room2
|
|
||||||
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
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Paging up", () => {
|
|
||||||
// XXX: Fails because flaky test https://github.com/vector-im/element-web/issues/26437
|
|
||||||
it.skip("Paging up through old messages after a room is read leaves the room read", () => {
|
|
||||||
// Given lots of messages are in the room, but we have read them
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, many("Msg", 110));
|
|
||||||
assertUnread(room2, 110);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When we restart, so only recent messages are loaded
|
|
||||||
saveAndReload();
|
|
||||||
goTo(room2);
|
|
||||||
assertMessageNotLoaded("Msg0010");
|
|
||||||
|
|
||||||
// And we page up, loading in old messages
|
|
||||||
pageUp();
|
|
||||||
cy.wait(200);
|
|
||||||
pageUp();
|
|
||||||
cy.wait(200);
|
|
||||||
pageUp();
|
|
||||||
assertMessageLoaded("Msg0010");
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Paging up through old messages of an unread room leaves the room unread", () => {
|
|
||||||
// Given lots of messages are in the room, and they are not read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, many("x\ny\nz\nMsg", 40)); // newline to spread out messages
|
|
||||||
assertUnread(room2, 40);
|
|
||||||
|
|
||||||
// When I jump to a message in the middle and page up
|
|
||||||
jumpTo(room2, "x\ny\nz\nMsg0020");
|
|
||||||
pageUp();
|
|
||||||
|
|
||||||
// Then the room is still unread
|
|
||||||
assertUnreadGreaterThan(room2, 1);
|
|
||||||
});
|
|
||||||
it("Paging up to find old threads that were previously read leaves the room read", () => {
|
|
||||||
// Given lots of messages in threads are all read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root1",
|
|
||||||
"Root2",
|
|
||||||
"Root3",
|
|
||||||
...manyThreadedOff("Root1", many("T", 20)),
|
|
||||||
...manyThreadedOff("Root2", many("T", 20)),
|
|
||||||
...manyThreadedOff("Root3", many("T", 20)),
|
|
||||||
]);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 60);
|
|
||||||
openThread("Root1");
|
|
||||||
assertUnread(room2, 40);
|
|
||||||
assertReadThread("Root1");
|
|
||||||
openThread("Root2");
|
|
||||||
assertUnread(room2, 20);
|
|
||||||
assertReadThread("Root2");
|
|
||||||
openThread("Root3");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root3");
|
|
||||||
|
|
||||||
// When I restart and page up to load old thread roots
|
|
||||||
goTo(room1);
|
|
||||||
saveAndReload();
|
|
||||||
goTo(room2);
|
|
||||||
pageUp();
|
|
||||||
|
|
||||||
// Then the room and threads remain read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root1");
|
|
||||||
assertReadThread("Root2");
|
|
||||||
assertReadThread("Root3");
|
|
||||||
});
|
|
||||||
it("Paging up to find old threads that were never read keeps the room unread", () => {
|
|
||||||
// Flaky with rust crypto
|
|
||||||
// See https://github.com/vector-im/element-web/issues/26539
|
|
||||||
skipIfRustCrypto();
|
|
||||||
|
|
||||||
// Given lots of messages in threads that are unread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root1",
|
|
||||||
"Root2",
|
|
||||||
"Root3",
|
|
||||||
...manyThreadedOff("Root1", many("T", 2)),
|
|
||||||
...manyThreadedOff("Root2", many("T", 2)),
|
|
||||||
...manyThreadedOff("Root3", many("T", 2)),
|
|
||||||
...many("Msg", 100),
|
|
||||||
]);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
assertUnreadThread("Root1");
|
|
||||||
assertUnreadThread("Root2");
|
|
||||||
assertUnreadThread("Root3");
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room remembers it's unread
|
|
||||||
// TODO: I (andyb) think this will fall in an encrypted room
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
|
|
||||||
// And when I page up to load old thread roots
|
|
||||||
goTo(room2);
|
|
||||||
pageUp();
|
|
||||||
|
|
||||||
// Then the room remains unread
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
assertUnreadThread("Root1");
|
|
||||||
assertUnreadThread("Root2");
|
|
||||||
assertUnreadThread("Root3");
|
|
||||||
});
|
|
||||||
// XXX: fails because flaky: https://github.com/vector-im/element-web/issues/26331
|
|
||||||
it.skip("Looking in thread view to find old threads that were never read makes the room unread", () => {
|
|
||||||
// Given lots of messages in threads that are unread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root1",
|
|
||||||
"Root2",
|
|
||||||
"Root3",
|
|
||||||
...manyThreadedOff("Root1", many("T", 2)),
|
|
||||||
...manyThreadedOff("Root2", many("T", 2)),
|
|
||||||
...manyThreadedOff("Root3", many("T", 2)),
|
|
||||||
...many("Msg", 100),
|
|
||||||
]);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
assertUnreadThread("Root1");
|
|
||||||
assertUnreadThread("Root2");
|
|
||||||
assertUnreadThread("Root3");
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room remembers it's unread
|
|
||||||
// TODO: I (andyb) think this will fall in an encrypted room
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
|
|
||||||
// And when I open the threads view
|
|
||||||
goTo(room2);
|
|
||||||
openThreadList();
|
|
||||||
|
|
||||||
// Then the room remains unread
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
assertUnreadThread("Root1");
|
|
||||||
assertUnreadThread("Root2");
|
|
||||||
assertUnreadThread("Root3");
|
|
||||||
});
|
|
||||||
it("After marking room as read, paging up to find old threads that were never read leaves the room read", () => {
|
|
||||||
// Flaky with rust crypto
|
|
||||||
// See https://github.com/vector-im/element-web/issues/26341
|
|
||||||
skipIfRustCrypto();
|
|
||||||
|
|
||||||
// Given lots of messages in threads that are unread but I marked as read on a main timeline message
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root1",
|
|
||||||
"Root2",
|
|
||||||
"Root3",
|
|
||||||
...manyThreadedOff("Root1", many("T", 2)),
|
|
||||||
...manyThreadedOff("Root2", many("T", 2)),
|
|
||||||
...manyThreadedOff("Root3", many("T", 2)),
|
|
||||||
...many("Msg", 100),
|
|
||||||
]);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room remembers it's read
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// And when I page up to load old thread roots
|
|
||||||
goTo(room2);
|
|
||||||
pageUp();
|
|
||||||
pageUp();
|
|
||||||
pageUp();
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
assertReadThread("Root1");
|
|
||||||
assertReadThread("Root2");
|
|
||||||
assertReadThread("Root3");
|
|
||||||
});
|
|
||||||
// XXX: fails because we see a dot instead of an unread number - probably the server and client disagree
|
|
||||||
it.skip("After marking room as read based on a thread message, opening threads view to find old threads that were never read leaves the room read", () => {
|
|
||||||
// Given lots of messages in threads that are unread but I marked as read on a thread message
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root1",
|
|
||||||
"Root2",
|
|
||||||
"Root3",
|
|
||||||
...manyThreadedOff("Root1", many("T1-", 2)),
|
|
||||||
...manyThreadedOff("Root2", many("T2-", 2)),
|
|
||||||
...manyThreadedOff("Root3", many("T3-", 2)),
|
|
||||||
...many("Msg", 100),
|
|
||||||
threadedOff("Msg0099", "Thread off 99"),
|
|
||||||
]);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room remembers it's read
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// And when I page up to load old thread roots
|
|
||||||
goTo(room2);
|
|
||||||
openThreadList();
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
assertReadThread("Root1");
|
|
||||||
assertReadThread("Root2");
|
|
||||||
assertReadThread("Root3");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Room list order", () => {
|
|
||||||
it.skip("Rooms with unread threads appear at the top of room list if 'unread first' is selected", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Notifications", () => {
|
|
||||||
describe("in the main timeline", () => {
|
|
||||||
it.skip("A new message that mentions me shows a notification", () => {});
|
|
||||||
it.skip("Reading a notifying message reduces the notification count in the room list, space and tab", () => {});
|
|
||||||
it.skip("Reading the last notifying message removes the notification marker from room list, space and tab", () => {});
|
|
||||||
it.skip("Editing a message to mentions me shows a notification", () => {});
|
|
||||||
it.skip("Reading the last notifying edited message removes the notification marker", () => {});
|
|
||||||
it.skip("Redacting a notifying message removes the notification marker", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("in threads", () => {
|
|
||||||
it.skip("A new threaded message that mentions me shows a notification", () => {});
|
|
||||||
it.skip("Reading a notifying threaded message removes the notification count", () => {});
|
|
||||||
it.skip("Notification count remains steady when reading threads that contain seen notifications", () => {});
|
|
||||||
it.skip("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {});
|
|
||||||
it.skip("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {});
|
|
||||||
it.skip("Redacting a notifying threaded message removes the notification marker", () => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,116 +0,0 @@
|
||||||
/*
|
|
||||||
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. */
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import {
|
|
||||||
goTo,
|
|
||||||
Message,
|
|
||||||
MessageContentSpec,
|
|
||||||
MessageFinder,
|
|
||||||
ReadReceiptSetup,
|
|
||||||
sendMessageAsClient,
|
|
||||||
} from "./read-receipts-utils";
|
|
||||||
|
|
||||||
describe("Read receipts", () => {
|
|
||||||
const roomAlpha = "Room Alpha";
|
|
||||||
const roomBeta = "Room Beta";
|
|
||||||
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
let messageFinder: MessageFinder;
|
|
||||||
let testSetup: ReadReceiptSetup;
|
|
||||||
|
|
||||||
function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.threadedOff(rootMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Note: unusually for the Cypress tests in this repo, we share a single
|
|
||||||
// Synapse between all the tests in this file.
|
|
||||||
//
|
|
||||||
// Stopping and starting Synapse costs about 0.25 seconds per test, so
|
|
||||||
// for most suites this is worth the cost for the extra assurance that
|
|
||||||
// each test is independent.
|
|
||||||
//
|
|
||||||
// Because there are so many tests in this file, and because sharing a
|
|
||||||
// Synapse should have no effect (because we create new rooms and users
|
|
||||||
// for each test), we share it here, saving ~30 seconds per run at time
|
|
||||||
// of writing.
|
|
||||||
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messageFinder = new MessageFinder();
|
|
||||||
testSetup = new ReadReceiptSetup(homeserver, "Mae", "Other User", roomAlpha, roomBeta);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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`
|
|
||||||
*/
|
|
||||||
function receiveMessages(room: string, messages: Message[]) {
|
|
||||||
sendMessageAsClient(testSetup.bot, room, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const room1 = roomAlpha;
|
|
||||||
const room2 = roomBeta;
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("messages with missing referents", () => {
|
|
||||||
it.skip("A message in an unknown thread is not visible and the room is read", () => {
|
|
||||||
// Given a thread existed and the room is read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root1", 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
|
|
||||||
});
|
|
||||||
it.skip("When a message's thread root appears later the thread appears and the room is unread", () => {});
|
|
||||||
it.skip("An edit of an unknown message is not visible and the room is read", () => {});
|
|
||||||
it.skip("When an edit's message appears later the edited version appears and the room is unread", () => {});
|
|
||||||
it.skip("A reaction to an unknown message is not visible and the room is read", () => {});
|
|
||||||
it.skip("When an reactions's message appears later it appears and the room is unread", () => {});
|
|
||||||
// Harder: validate that we request the messages we are missing?
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("receipts with missing events", () => {
|
|
||||||
// Later: when we have order in receipts, we can change these tests to
|
|
||||||
// make receipts still work, even when their message is not found.
|
|
||||||
it.skip("A receipt for an unknown message does not change the state of an unread room", () => {});
|
|
||||||
it.skip("A receipt for an unknown message does not change the state of a read room", () => {});
|
|
||||||
it.skip("A threaded receipt for an unknown message does not change the state of an unread thread", () => {});
|
|
||||||
it.skip("A threaded receipt for an unknown message does not change the state of a read thread", () => {});
|
|
||||||
it.skip("A threaded receipt for an unknown thread does not change the state of an unread thread", () => {});
|
|
||||||
it.skip("A threaded receipt for an unknown thread does not change the state of a read thread", () => {});
|
|
||||||
it.skip("A threaded receipt for a message on main does not change the state of an unread room", () => {});
|
|
||||||
it.skip("A threaded receipt for a message on main does not change the state of a read room", () => {});
|
|
||||||
it.skip("A main receipt for a message on a thread does not change the state of an unread room", () => {});
|
|
||||||
it.skip("A main receipt for a message on a thread does not change the state of a read room", () => {});
|
|
||||||
it.skip("A threaded receipt for a thread root does not mark it as read", () => {});
|
|
||||||
// Harder: validate that we request the messages we are missing?
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,503 +0,0 @@
|
||||||
/*
|
|
||||||
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. */
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import {
|
|
||||||
assertRead,
|
|
||||||
assertReadThread,
|
|
||||||
assertUnread,
|
|
||||||
assertUnreadLessThan,
|
|
||||||
assertUnreadThread,
|
|
||||||
backToThreadsList,
|
|
||||||
ReadReceiptSetup,
|
|
||||||
goTo,
|
|
||||||
many,
|
|
||||||
markAsRead,
|
|
||||||
Message,
|
|
||||||
MessageContentSpec,
|
|
||||||
MessageFinder,
|
|
||||||
openThread,
|
|
||||||
saveAndReload,
|
|
||||||
sendMessageAsClient,
|
|
||||||
} from "./read-receipts-utils";
|
|
||||||
|
|
||||||
describe("Read receipts", () => {
|
|
||||||
const roomAlpha = "Room Alpha";
|
|
||||||
const roomBeta = "Room Beta";
|
|
||||||
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
let messageFinder: MessageFinder;
|
|
||||||
let testSetup: ReadReceiptSetup;
|
|
||||||
|
|
||||||
function replyTo(targetMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.replyTo(targetMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.threadedOff(rootMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function manyThreadedOff(rootMessage: string, newMessages: Array<string>): Array<MessageContentSpec> {
|
|
||||||
return messageFinder.manyThreadedOff(rootMessage, newMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
function jumpTo(room: string, message: string, includeThreads = false) {
|
|
||||||
return messageFinder.jumpTo(room, message, includeThreads);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Note: unusually for the Cypress tests in this repo, we share a single
|
|
||||||
// Synapse between all the tests in this file.
|
|
||||||
//
|
|
||||||
// Stopping and starting Synapse costs about 0.25 seconds per test, so
|
|
||||||
// for most suites this is worth the cost for the extra assurance that
|
|
||||||
// each test is independent.
|
|
||||||
//
|
|
||||||
// Because there are so many tests in this file, and because sharing a
|
|
||||||
// Synapse should have no effect (because we create new rooms and users
|
|
||||||
// for each test), we share it here, saving ~30 seconds per run at time
|
|
||||||
// of writing.
|
|
||||||
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messageFinder = new MessageFinder();
|
|
||||||
testSetup = new ReadReceiptSetup(homeserver, "Mae", "Other User", roomAlpha, roomBeta);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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`
|
|
||||||
*/
|
|
||||||
function receiveMessages(room: string, messages: Message[]) {
|
|
||||||
sendMessageAsClient(testSetup.bot, room, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends messages into given room as the currently logged-in user
|
|
||||||
* @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`
|
|
||||||
*/
|
|
||||||
function sendMessages(room: string, messages: Message[]) {
|
|
||||||
cy.getClient().then((cli) => sendMessageAsClient(cli, room, messages));
|
|
||||||
}
|
|
||||||
|
|
||||||
const room1 = roomAlpha;
|
|
||||||
const room2 = roomBeta;
|
|
||||||
|
|
||||||
describe("new messages", () => {
|
|
||||||
describe("in the main timeline", () => {
|
|
||||||
it("Receiving a message makes a room unread", () => {
|
|
||||||
// Given I am in a different room
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I receive some messages
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
|
|
||||||
// Then the room is marked as unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Reading latest message makes the room read", () => {
|
|
||||||
// Given I have some unread messages
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then the room becomes read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails (sometimes!) because the unread count stays high
|
|
||||||
it.skip("Reading an older message leaves the room unread", () => {
|
|
||||||
// Given there are lots of messages in a room
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, many("Msg", 30));
|
|
||||||
assertUnread(room2, 30);
|
|
||||||
|
|
||||||
// When I jump to one of the older messages
|
|
||||||
jumpTo(room2, "Msg0001");
|
|
||||||
|
|
||||||
// Then the room is still unread, but some messages were read
|
|
||||||
assertUnreadLessThan(room2, 30);
|
|
||||||
});
|
|
||||||
it("Marking a room as read makes it read", () => {
|
|
||||||
// Given I have some unread messages
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then it is read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Receiving a new message after marking as read makes it unread", () => {
|
|
||||||
// Given I have marked my messages as read
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I receive a new message
|
|
||||||
receiveMessages(room2, ["Msg2"]);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("A room with a new message is still unread after restart", () => {
|
|
||||||
// Given I have an unread message
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then I still have an unread message
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("A room where all messages are read is still read after restart", () => {
|
|
||||||
// Given I have read all messages
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then all messages are still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("A room that was marked as read is still read after restart", () => {
|
|
||||||
// Given I have marked all messages as read
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then all messages are still read
|
|
||||||
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.
|
|
||||||
it.skip("Me sending a message from a different client marks room as read", () => {
|
|
||||||
// Given I have unread messages
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I send a new message from a different client
|
|
||||||
sendMessages(room2, ["Msg2"]);
|
|
||||||
|
|
||||||
// Then this room is marked as read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("in threads", () => {
|
|
||||||
it("Receiving a message makes a room unread", () => {
|
|
||||||
// Given a message arrived and is read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When I receive a threaded message
|
|
||||||
receiveMessages(room2, [threadedOff("Msg1", "Resp1")]);
|
|
||||||
|
|
||||||
// Then the room becomes unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Reading the last threaded message makes the room read", () => {
|
|
||||||
// Given a thread exists and is not read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// When I read it
|
|
||||||
openThread("Msg1");
|
|
||||||
|
|
||||||
// The room becomes read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Reading a thread message makes the thread read", () => {
|
|
||||||
// Given a thread exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
|
|
||||||
assertUnread(room2, 3); // (Sanity)
|
|
||||||
|
|
||||||
// When I read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then room does appear unread
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// Until we open the thread
|
|
||||||
openThread("Msg1");
|
|
||||||
assertReadThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Reading an older thread message leaves the thread unread", () => {
|
|
||||||
// Given there are many messages in a thread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["ThreadRoot", ...manyThreadedOff("ThreadRoot", many("InThread", 20))]);
|
|
||||||
assertUnread(room2, 21);
|
|
||||||
|
|
||||||
// When I read an older message in the thread
|
|
||||||
jumpTo(room2, "InThread0001", true);
|
|
||||||
assertUnreadLessThan(room2, 21);
|
|
||||||
// TODO: for some reason, we can't find the first message
|
|
||||||
// "InThread0", so I am using the second here. Also, they appear
|
|
||||||
// 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
|
|
||||||
backToThreadsList();
|
|
||||||
assertUnreadThread("ThreadRoot");
|
|
||||||
});
|
|
||||||
it("Reading only one thread's message does not make the room read", () => {
|
|
||||||
// Given two threads are unread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]);
|
|
||||||
assertUnread(room2, 4);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I only read one of them
|
|
||||||
openThread("Msg1");
|
|
||||||
|
|
||||||
// The room is still unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Reading only one thread's message makes that thread read but not others", () => {
|
|
||||||
// Given I have unread threads
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2", threadedOff("Msg1", "Resp1"), threadedOff("Msg2", "Resp2")]);
|
|
||||||
assertUnread(room2, 4); // (Sanity)
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
assertUnreadThread("Msg1");
|
|
||||||
assertUnreadThread("Msg2");
|
|
||||||
|
|
||||||
// When I read one of them
|
|
||||||
openThread("Msg1");
|
|
||||||
|
|
||||||
// Then that one is read, but the other is not
|
|
||||||
assertReadThread("Msg1");
|
|
||||||
assertUnreadThread("Msg2");
|
|
||||||
});
|
|
||||||
it("Reading the main timeline does not mark a thread message as read", () => {
|
|
||||||
// Given a thread exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
|
|
||||||
assertUnread(room2, 3); // (Sanity)
|
|
||||||
|
|
||||||
// When I read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// Then thread does appear unread
|
|
||||||
assertUnreadThread("Msg1");
|
|
||||||
});
|
|
||||||
it("Marking a room with unread threads as read makes it read", () => {
|
|
||||||
// Given I have an unread thread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
|
|
||||||
assertUnread(room2, 3); // (Sanity)
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then the room is read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Sending a new thread message after marking as read makes it unread", () => {
|
|
||||||
// Given a thread exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// Then another message appears in the thread
|
|
||||||
receiveMessages(room2, [threadedOff("Msg1", "Resp3")]);
|
|
||||||
|
|
||||||
// Then the room becomes unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Sending a new different-thread message after marking as read makes it unread", () => {
|
|
||||||
// Given 2 threads exist, and Thread2 has the latest message in it
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Thread1", "Thread2", threadedOff("Thread1", "t1a")]);
|
|
||||||
// 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.
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
|
|
||||||
receiveMessages(room2, [threadedOff("Thread2", "t2a")]);
|
|
||||||
// Make sure the 4th message has arrived before we mark as read.
|
|
||||||
assertUnread(room2, 4);
|
|
||||||
|
|
||||||
// When I mark the room as read (making an unthreaded receipt for t2a)
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// Then another message appears in the other thread
|
|
||||||
receiveMessages(room2, [threadedOff("Thread1", "t1b")]);
|
|
||||||
|
|
||||||
// Then the room becomes unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("A room with a new threaded message is still unread after restart", () => {
|
|
||||||
// Given a thread exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
|
|
||||||
assertUnread(room2, 3); // (Sanity)
|
|
||||||
|
|
||||||
// When I read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then room does appear unread
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
saveAndReload();
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// Until we open the thread
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("A room where all threaded messages are read is still read after restart", () => {
|
|
||||||
// Given I have read all the threads
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]);
|
|
||||||
assertUnread(room2, 3); // (Sanity)
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("thread roots", () => {
|
|
||||||
it("Reading a thread root does not mark the thread as read", () => {
|
|
||||||
// Given a thread exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]);
|
|
||||||
assertUnread(room2, 2); // (Sanity)
|
|
||||||
|
|
||||||
// When I read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then room does appear unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
assertUnreadThread("Msg1");
|
|
||||||
});
|
|
||||||
// XXX: fails because we jump to the wrong place in the timeline
|
|
||||||
it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {
|
|
||||||
// Given lots of messages are on the main timeline, and one has a thread off it
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
...many("beforeThread", 30),
|
|
||||||
"ThreadRoot",
|
|
||||||
threadedOff("ThreadRoot", "InThread"),
|
|
||||||
...many("afterThread", 30),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 62); // Sanity
|
|
||||||
|
|
||||||
// When I jump to an old message and read the thread
|
|
||||||
jumpTo(room2, "beforeThread0000");
|
|
||||||
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.
|
|
||||||
assertUnread(room2, 30);
|
|
||||||
});
|
|
||||||
it("Creating a new thread based on a reply makes the room unread", () => {
|
|
||||||
// Given a message and reply exist and are read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I receive a thread message created on the reply
|
|
||||||
receiveMessages(room2, [threadedOff("Reply1", "Resp1")]);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Reading a thread whose root is a reply makes the room read", () => {
|
|
||||||
// Given an unread thread off a reply exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
assertUnreadThread("Reply1");
|
|
||||||
|
|
||||||
// When I read the thread
|
|
||||||
openThread("Reply1");
|
|
||||||
|
|
||||||
// Then the room and thread are read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Reply1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,354 +0,0 @@
|
||||||
/*
|
|
||||||
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. */
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import {
|
|
||||||
assertRead,
|
|
||||||
assertReadThread,
|
|
||||||
assertStillRead,
|
|
||||||
assertUnread,
|
|
||||||
BotActionSpec,
|
|
||||||
goTo,
|
|
||||||
markAsRead,
|
|
||||||
Message,
|
|
||||||
MessageContentSpec,
|
|
||||||
MessageFinder,
|
|
||||||
openThread,
|
|
||||||
ReadReceiptSetup,
|
|
||||||
saveAndReload,
|
|
||||||
sendMessageAsClient,
|
|
||||||
} from "./read-receipts-utils";
|
|
||||||
|
|
||||||
describe("Read receipts", () => {
|
|
||||||
const roomAlpha = "Room Alpha";
|
|
||||||
const roomBeta = "Room Beta";
|
|
||||||
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
let messageFinder: MessageFinder;
|
|
||||||
let testSetup: ReadReceiptSetup;
|
|
||||||
|
|
||||||
function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.threadedOff(rootMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reactionTo(targetMessage: string, reaction: string): BotActionSpec {
|
|
||||||
return messageFinder.reactionTo(targetMessage, reaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Note: unusually for the Cypress tests in this repo, we share a single
|
|
||||||
// Synapse between all the tests in this file.
|
|
||||||
//
|
|
||||||
// Stopping and starting Synapse costs about 0.25 seconds per test, so
|
|
||||||
// for most suites this is worth the cost for the extra assurance that
|
|
||||||
// each test is independent.
|
|
||||||
//
|
|
||||||
// Because there are so many tests in this file, and because sharing a
|
|
||||||
// Synapse should have no effect (because we create new rooms and users
|
|
||||||
// for each test), we share it here, saving ~30 seconds per run at time
|
|
||||||
// of writing.
|
|
||||||
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messageFinder = new MessageFinder();
|
|
||||||
testSetup = new ReadReceiptSetup(homeserver, "Mae", "Other User", roomAlpha, roomBeta);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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`
|
|
||||||
*/
|
|
||||||
function receiveMessages(room: string, messages: Message[]) {
|
|
||||||
sendMessageAsClient(testSetup.bot, room, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const room1 = roomAlpha;
|
|
||||||
const room2 = roomBeta;
|
|
||||||
|
|
||||||
describe("reactions", () => {
|
|
||||||
describe("in the main timeline", () => {
|
|
||||||
it("Receiving a reaction to a message does not make a room unread", () => {
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [reactionTo("Msg2", "🪿")]);
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Reacting to a message after marking as read does not make the room unread", () => {
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
receiveMessages(room2, [reactionTo("Msg2", "🪿")]);
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("A room with an unread reaction is still read after restart", () => {
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
receiveMessages(room2, [reactionTo("Msg2", "🪿")]);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
saveAndReload();
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("A room where all reactions are read is still read after restart", () => {
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2", reactionTo("Msg2", "🪿")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
saveAndReload();
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("in threads", () => {
|
|
||||||
it("A reaction to a threaded message does not make the room unread", () => {
|
|
||||||
// Given a thread exists and I have read it
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When someone reacts to a thread message
|
|
||||||
receiveMessages(room2, [reactionTo("Reply1", "🪿")]);
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because the room is still "bold" even though the notification counts all disappear
|
|
||||||
it.skip("Marking a room as read after a reaction in a thread makes it read", () => {
|
|
||||||
// Given a thread exists with a reaction
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1"), reactionTo("Reply1", "🪿")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then it becomes read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because the room is still "bold" even though the notification counts all disappear
|
|
||||||
it.skip("Reacting to a thread message after marking as read does not make the room unread", () => {
|
|
||||||
// Given a thread exists and I have marked it as read
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1"), reactionTo("Reply1", "🪿")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When someone reacts to a thread message
|
|
||||||
receiveMessages(room2, [reactionTo("Reply1", "🪿")]);
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it.skip("A room with a reaction to a threaded message is still unread after restart", () => {
|
|
||||||
// Given a thread exists and I have read it
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// And someone reacted to it, which doesn't stop it being read
|
|
||||||
receiveMessages(room2, [reactionTo("Reply1", "🪿")]);
|
|
||||||
assertStillRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("A room where all reactions in threads are read is still read after restart", () => {
|
|
||||||
// Given multiple threads with reactions exist and are read
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Msg1",
|
|
||||||
threadedOff("Msg1", "Reply1a"),
|
|
||||||
reactionTo("Reply1a", "r"),
|
|
||||||
"Msg2",
|
|
||||||
threadedOff("Msg1", "Reply1b"),
|
|
||||||
threadedOff("Msg2", "Reply2a"),
|
|
||||||
reactionTo("Msg1", "e"),
|
|
||||||
threadedOff("Msg2", "Reply2b"),
|
|
||||||
reactionTo("Reply2a", "a"),
|
|
||||||
reactionTo("Reply2b", "c"),
|
|
||||||
reactionTo("Reply1b", "t"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 6);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertReadThread("Msg1");
|
|
||||||
openThread("Msg2");
|
|
||||||
assertReadThread("Msg2");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Msg1");
|
|
||||||
assertReadThread("Msg2");
|
|
||||||
});
|
|
||||||
it("Can remove a reaction in a thread", () => {
|
|
||||||
// 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
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1a")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I react to a thread message
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
cy.get(".mx_ThreadPanel").findByText("Reply1a").realHover();
|
|
||||||
cy.findByRole("button", { name: "React" }).click();
|
|
||||||
cy.get(".mx_EmojiPicker_body").findByText("😀").click();
|
|
||||||
|
|
||||||
// And cancel the reaction
|
|
||||||
cy.get(".mx_ThreadPanel").findByLabelText("Mae reacted with 😀").click();
|
|
||||||
|
|
||||||
// Then it disappears
|
|
||||||
cy.get(".mx_ThreadPanel").findByLabelText("Mae reacted with 😀").should("not.exist");
|
|
||||||
|
|
||||||
// And I can do it all again without an error
|
|
||||||
cy.get(".mx_ThreadPanel").findByText("Reply1a").realHover();
|
|
||||||
cy.findByRole("button", { name: "React" }).click();
|
|
||||||
cy.get(".mx_EmojiPicker_body").findAllByText("😀").first().click();
|
|
||||||
cy.get(".mx_ThreadPanel").findByLabelText("Mae reacted with 😀").click();
|
|
||||||
cy.get(".mx_ThreadPanel").findByLabelText("Mae reacted with 😀").should("not.exist");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("thread roots", () => {
|
|
||||||
it("A reaction to a thread root does not make the room unread", () => {
|
|
||||||
// Given a read thread root exists
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When someone reacts to it
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [reactionTo("Msg1", "🪿")]);
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Reading a reaction to a thread root leaves the room read", () => {
|
|
||||||
// Given a read thread root exists
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// And the reaction to it does not make us unread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [reactionTo("Msg1", "🪿")]);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When we read the reaction and go away again
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Msg1");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because the room is still "bold" even though the notification counts all disappear
|
|
||||||
it.skip("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {
|
|
||||||
// Given a thread root exists
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// And we have marked the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When someone reacts to it
|
|
||||||
receiveMessages(room2, [reactionTo("Msg1", "🪿")]);
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,585 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { MatrixClient, MatrixEvent, Room, IndexedDBStore } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import Chainable = Cypress.Chainable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 class ReadReceiptSetup {
|
|
||||||
roomAlpha: string;
|
|
||||||
roomBeta: string;
|
|
||||||
alphaRoomId: string;
|
|
||||||
betaRoomId: string;
|
|
||||||
bot: MatrixClient;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
homeserver: HomeserverInstance,
|
|
||||||
userName: string,
|
|
||||||
botName: string,
|
|
||||||
roomAlpha: string,
|
|
||||||
roomBeta: string,
|
|
||||||
) {
|
|
||||||
this.roomAlpha = roomAlpha;
|
|
||||||
this.roomBeta = roomBeta;
|
|
||||||
|
|
||||||
// Create a user
|
|
||||||
cy.initTestUser(homeserver, userName)
|
|
||||||
// Create 2 rooms
|
|
||||||
.then(() => {
|
|
||||||
cy.createRoom({ name: roomAlpha }).then((createdRoomId) => {
|
|
||||||
this.alphaRoomId = createdRoomId;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
cy.createRoom({ name: roomBeta }).then((createdRoomId) => {
|
|
||||||
this.betaRoomId = createdRoomId;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// Create a bot
|
|
||||||
.then(() => {
|
|
||||||
cy.getBot(homeserver, { displayName: botName }).then((botClient) => {
|
|
||||||
this.bot = botClient;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// Invite the bot to both rooms
|
|
||||||
.then(() => {
|
|
||||||
cy.inviteUser(this.alphaRoomId, this.bot.getUserId());
|
|
||||||
cy.viewRoomById(this.alphaRoomId);
|
|
||||||
cy.get(".mx_LegacyRoomHeader").within(() => cy.findByTitle(roomAlpha).should("exist"));
|
|
||||||
cy.findByText(botName + " joined the room", { timeout: 20000 }).should("exist");
|
|
||||||
|
|
||||||
cy.inviteUser(this.betaRoomId, this.bot.getUserId());
|
|
||||||
cy.viewRoomById(this.betaRoomId);
|
|
||||||
cy.get(".mx_LegacyRoomHeader").within(() => cy.findByTitle(roomBeta).should("exist"));
|
|
||||||
cy.findByText(botName + " joined the room", { timeout: 20000 }).should("exist");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 MessageFinder {
|
|
||||||
/**
|
|
||||||
* Map of message content -> event.
|
|
||||||
*/
|
|
||||||
messages = new Map<String, 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: Room, message: string, includeThreads = false): Promise<MatrixEvent> {
|
|
||||||
const cached = this.messages.get(message);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
this.messages.set(message, ev);
|
|
||||||
return ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
room.on("Room.timeline" as any, (ev: MatrixEvent) => {
|
|
||||||
if (ev.getContent().body === message) {
|
|
||||||
this.messages.set(message, ev);
|
|
||||||
resolve(ev);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: Room): Promise<Record<string, unknown>> {
|
|
||||||
const ev = await this.messageFinder?.getMessage(room, originalMessage, true);
|
|
||||||
|
|
||||||
// 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(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})(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: Room): Promise<Record<string, unknown>> {
|
|
||||||
const ev = await this.messageFinder.getMessage(room, targetMessage, true);
|
|
||||||
|
|
||||||
return {
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": newMessage,
|
|
||||||
"m.relates_to": {
|
|
||||||
"m.in_reply_to": {
|
|
||||||
event_id: ev.getId(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})(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: Room): Promise<Record<string, unknown>> {
|
|
||||||
const ev = await this.messageFinder.getMessage(room, rootMessage);
|
|
||||||
|
|
||||||
return {
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": newMessage,
|
|
||||||
"m.relates_to": {
|
|
||||||
event_id: ev.getId(),
|
|
||||||
is_falling_back: true,
|
|
||||||
rel_type: "m.thread",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})(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(cli: MatrixClient, room: Room): Promise<void> {
|
|
||||||
const ev = await this.messageFinder.getMessage(room, targetMessage, true);
|
|
||||||
const threadId = !ev.isThreadRoot ? ev.threadRootId : undefined;
|
|
||||||
await cli.sendEvent(room.roomId, threadId ?? null, "m.reaction", {
|
|
||||||
"m.relates_to": {
|
|
||||||
rel_type: "m.annotation",
|
|
||||||
event_id: ev.getId(),
|
|
||||||
key: reaction,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BotActionSpec to send a redaction into a room
|
|
||||||
* @param messageFinder - used to find the existing event
|
|
||||||
* @param targetMessage - the body of the message to send a redaction to
|
|
||||||
*/
|
|
||||||
redactionOf(targetMessage: string): BotActionSpec {
|
|
||||||
return new (class extends BotActionSpec {
|
|
||||||
public async performAction(cli: MatrixClient, room: Room): Promise<void> {
|
|
||||||
const ev = await this.messageFinder.getMessage(room, targetMessage, true);
|
|
||||||
await cli.redactEvent(room.roomId, ev.threadRootId, ev.getId());
|
|
||||||
}
|
|
||||||
})(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find and display a message.
|
|
||||||
*
|
|
||||||
* @param room 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
|
|
||||||
*/
|
|
||||||
jumpTo(room: string, message: string, includeThreads = false) {
|
|
||||||
cy.log("Jump to message", room, message, includeThreads);
|
|
||||||
cy.getClient().then((cli) => {
|
|
||||||
findRoomByName(room).then(async ({ roomId }) => {
|
|
||||||
const roomObject = cli.getRoom(roomId);
|
|
||||||
const foundMessage = await this.getMessage(roomObject, message, includeThreads);
|
|
||||||
cy.visit(`/#/room/${roomId}/${foundMessage.getId()}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Something that can provide the content of a message.
|
|
||||||
*
|
|
||||||
* For example, we return and instance of this from {@link
|
|
||||||
* MessageFinder.replyTo} which creates a reply based on a previous message.
|
|
||||||
*/
|
|
||||||
export abstract class MessageContentSpec {
|
|
||||||
messageFinder: MessageFinder | null;
|
|
||||||
|
|
||||||
constructor(messageFinder: MessageFinder = null) {
|
|
||||||
this.messageFinder = messageFinder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract getContent(room: 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
|
|
||||||
* MessageFinder.redactionOf} which redacts the message we are referring to.
|
|
||||||
*/
|
|
||||||
export abstract class BotActionSpec {
|
|
||||||
messageFinder: MessageFinder | null;
|
|
||||||
|
|
||||||
constructor(messageFinder: MessageFinder = null) {
|
|
||||||
this.messageFinder = messageFinder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract performAction(cli: MatrixClient, room: 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the supplied client to send messages or perform actions as specified by
|
|
||||||
* the supplied {@link Message} items.
|
|
||||||
*/
|
|
||||||
export function sendMessageAsClient(cli: MatrixClient, room: string, messages: Message[]) {
|
|
||||||
const roomIdFinder = findRoomByName(room);
|
|
||||||
for (const message of messages) {
|
|
||||||
roomIdFinder.then(async (room) => {
|
|
||||||
if (typeof message === "string") {
|
|
||||||
await cli.sendTextMessage(room.roomId, message);
|
|
||||||
} else if (message instanceof MessageContentSpec) {
|
|
||||||
await cli.sendMessage(room.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.
|
|
||||||
cy.wait(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the room with the supplied name.
|
|
||||||
*/
|
|
||||||
export function goTo(room: string) {
|
|
||||||
cy.viewRoomByName(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findRoomByName(room: string): Chainable<Room> {
|
|
||||||
return cy.getClient().then((cli) => {
|
|
||||||
return cli.getRooms().find((r) => r.name === room);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click the thread with the supplied content in the thread root to open it in
|
|
||||||
* the Threads panel.
|
|
||||||
*/
|
|
||||||
export function openThread(rootMessage: string) {
|
|
||||||
cy.log("Open thread", rootMessage);
|
|
||||||
cy.get(".mx_RoomView_body", { log: false }).within(() => {
|
|
||||||
cy.findAllByText(rootMessage)
|
|
||||||
.filter(".mx_EventTile_body")
|
|
||||||
.parents(".mx_EventTile[data-scroll-tokens]")
|
|
||||||
.realHover()
|
|
||||||
.findByRole("button", { name: "Reply in thread", log: false })
|
|
||||||
.click();
|
|
||||||
});
|
|
||||||
cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the threads panel. (Actually, close any right panel, but for these
|
|
||||||
* tests we only open the threads panel.)
|
|
||||||
*/
|
|
||||||
export function closeThreadsPanel() {
|
|
||||||
cy.log("Close threads panel");
|
|
||||||
cy.get(".mx_RightPanel").findByTitle("Close").click();
|
|
||||||
cy.get(".mx_RightPanel").should("not.exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return to the list of threads, given we are viewing a single thread.
|
|
||||||
*/
|
|
||||||
export function backToThreadsList() {
|
|
||||||
cy.log("Back to threads list");
|
|
||||||
cy.get(".mx_RightPanel").findByTitle("Threads").click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: MatrixClient, room: Room): Promise<void> {
|
|
||||||
await cli.sendEvent(room.roomId, null, eventType, content);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoomListTile(room: string) {
|
|
||||||
return cy.findByRole("treeitem", { name: new RegExp("^" + room), log: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export function assertMessageLoaded(messagePart: string) {
|
|
||||||
cy.get(".mx_EventTile_body").contains(messagePart).should("exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export function assertMessageNotLoaded(messagePart: string) {
|
|
||||||
cy.get(".mx_EventTile_body").contains(messagePart).should("not.exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll the messages panel up 1000 pixels.
|
|
||||||
*/
|
|
||||||
export function pageUp() {
|
|
||||||
cy.get(".mx_RoomView_messagePanel").then((refs) =>
|
|
||||||
refs.each((_, messagePanel) => {
|
|
||||||
messagePanel.scrollTop -= 1000;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click the "Mark as Read" context menu item on the room with the supplied name
|
|
||||||
* in the room list.
|
|
||||||
*/
|
|
||||||
export function markAsRead(room: string) {
|
|
||||||
cy.log("Marking room as read", room);
|
|
||||||
getRoomListTile(room).rightclick();
|
|
||||||
cy.findByText("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.
|
|
||||||
*/
|
|
||||||
export function assertRead(room: string) {
|
|
||||||
cy.log("Assert room read", room);
|
|
||||||
return getRoomListTile(room).within(() => {
|
|
||||||
cy.get(".mx_NotificationBadge_dot").should("not.exist");
|
|
||||||
cy.get(".mx_NotificationBadge_count").should("not.exist");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.)
|
|
||||||
*/
|
|
||||||
export function assertStillRead(room: string) {
|
|
||||||
cy.wait(200);
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
export function assertUnread(room: string, count: number | ".") {
|
|
||||||
cy.log("Assert room unread", room, count);
|
|
||||||
return getRoomListTile(room).within(() => {
|
|
||||||
if (count === ".") {
|
|
||||||
cy.get(".mx_NotificationBadge_dot").should("exist");
|
|
||||||
} else {
|
|
||||||
cy.get(".mx_NotificationBadge_count").should("have.text", count);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
export function assertUnreadLessThan(room: string, lessThan: number) {
|
|
||||||
cy.log("Assert unread less than", room, lessThan);
|
|
||||||
return getRoomListTile(room).within(() => {
|
|
||||||
cy.get(".mx_NotificationBadge_count").should(($count) =>
|
|
||||||
expect(parseInt($count.get(0).textContent, 10)).to.be.lessThan(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
|
|
||||||
*/
|
|
||||||
export function assertUnreadGreaterThan(room: string, greaterThan: number) {
|
|
||||||
cy.log("Assert unread greater than", room, greaterThan);
|
|
||||||
return getRoomListTile(room).within(() => {
|
|
||||||
cy.get(".mx_NotificationBadge_count").should(($count) =>
|
|
||||||
expect(parseInt($count.get(0).textContent, 10)).to.be.greaterThan(greaterThan),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click the "Threads" or "Back" button if needed to get to the threads list.
|
|
||||||
*/
|
|
||||||
export function openThreadList() {
|
|
||||||
cy.log("Open threads list");
|
|
||||||
|
|
||||||
// If we've just entered the room, the threads panel takes a while to decide
|
|
||||||
// whether it's open or not - wait here to give it a chance to settle.
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
cy.findByTestId("threadsButton", { log: false }).then(($button) => {
|
|
||||||
if ($button?.attr("aria-current") !== "true") {
|
|
||||||
cy.findByTestId("threadsButton", { log: false }).click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".mx_ThreadPanel", { log: false })
|
|
||||||
.should("exist")
|
|
||||||
.then(($panel) => {
|
|
||||||
const $button = $panel.find('.mx_BaseCard_back[title="Threads"]');
|
|
||||||
// If the Threads back button is present then click it - the
|
|
||||||
// threads button can open either threads list or thread panel
|
|
||||||
if ($button.length) {
|
|
||||||
$button.trigger("click");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThreadListTile(rootMessage: string) {
|
|
||||||
openThreadList();
|
|
||||||
return cy.contains(".mx_ThreadPanel .mx_EventTile_body", rootMessage, { log: false }).closest("li");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that the thread with the supplied content in its root message is shown
|
|
||||||
* as read in the Threads list.
|
|
||||||
*/
|
|
||||||
export function assertReadThread(rootMessage: string) {
|
|
||||||
cy.log("Assert thread read", rootMessage);
|
|
||||||
return getThreadListTile(rootMessage).within(() => {
|
|
||||||
cy.get(".mx_NotificationBadge", { log: false }).should("not.exist");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that the thread with the supplied content in its root message is shown
|
|
||||||
* as unread in the Threads list.
|
|
||||||
*/
|
|
||||||
export function assertUnreadThread(rootMessage: string) {
|
|
||||||
cy.log("Assert unread thread", rootMessage);
|
|
||||||
return getThreadListTile(rootMessage).within(() => {
|
|
||||||
cy.get(".mx_NotificationBadge").should("exist");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save our indexeddb information and then refresh the page.
|
|
||||||
*/
|
|
||||||
export function saveAndReload() {
|
|
||||||
cy.log("Save and reload");
|
|
||||||
cy.getClient().then((cli) => {
|
|
||||||
// @ts-ignore
|
|
||||||
return (cli.store as IndexedDBStore).reallySave();
|
|
||||||
});
|
|
||||||
cy.reload();
|
|
||||||
// Wait for the app to reload
|
|
||||||
cy.log("Waiting for app to reload");
|
|
||||||
cy.get(".mx_RoomView", { log: false, timeout: 20000 }).should("exist");
|
|
||||||
}
|
|
|
@ -1,868 +0,0 @@
|
||||||
/*
|
|
||||||
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. */
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import {
|
|
||||||
assertRead,
|
|
||||||
assertReadThread,
|
|
||||||
assertStillRead,
|
|
||||||
assertUnread,
|
|
||||||
assertUnreadLessThan,
|
|
||||||
assertUnreadThread,
|
|
||||||
BotActionSpec,
|
|
||||||
closeThreadsPanel,
|
|
||||||
goTo,
|
|
||||||
markAsRead,
|
|
||||||
Message,
|
|
||||||
MessageContentSpec,
|
|
||||||
MessageFinder,
|
|
||||||
openThread,
|
|
||||||
ReadReceiptSetup,
|
|
||||||
saveAndReload,
|
|
||||||
sendMessageAsClient,
|
|
||||||
} from "./read-receipts-utils";
|
|
||||||
|
|
||||||
describe("Read receipts", () => {
|
|
||||||
const roomAlpha = "Room Alpha";
|
|
||||||
const roomBeta = "Room Beta";
|
|
||||||
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
let messageFinder: MessageFinder;
|
|
||||||
let testSetup: ReadReceiptSetup;
|
|
||||||
|
|
||||||
function editOf(originalMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.editOf(originalMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function replyTo(targetMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.replyTo(targetMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec {
|
|
||||||
return messageFinder.threadedOff(rootMessage, newMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reactionTo(targetMessage: string, reaction: string): BotActionSpec {
|
|
||||||
return messageFinder.reactionTo(targetMessage, reaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
function redactionOf(targetMessage: string): BotActionSpec {
|
|
||||||
return messageFinder.redactionOf(targetMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Note: unusually for the Cypress tests in this repo, we share a single
|
|
||||||
// Synapse between all the tests in this file.
|
|
||||||
//
|
|
||||||
// Stopping and starting Synapse costs about 0.25 seconds per test, so
|
|
||||||
// for most suites this is worth the cost for the extra assurance that
|
|
||||||
// each test is independent.
|
|
||||||
//
|
|
||||||
// Because there are so many tests in this file, and because sharing a
|
|
||||||
// Synapse should have no effect (because we create new rooms and users
|
|
||||||
// for each test), we share it here, saving ~30 seconds per run at time
|
|
||||||
// of writing.
|
|
||||||
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messageFinder = new MessageFinder();
|
|
||||||
testSetup = new ReadReceiptSetup(homeserver, "Mae", "Other User", roomAlpha, roomBeta);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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`
|
|
||||||
*/
|
|
||||||
function receiveMessages(room: string, messages: Message[]) {
|
|
||||||
sendMessageAsClient(testSetup.bot, room, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const room1 = roomAlpha;
|
|
||||||
const room2 = roomBeta;
|
|
||||||
|
|
||||||
describe("redactions", () => {
|
|
||||||
describe("in the main timeline", () => {
|
|
||||||
it("Redacting the message pointed to by my receipt leaves the room read", () => {
|
|
||||||
// Given I have read the messages in a room
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When the latest message is redacted
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
|
|
||||||
// Then the room remains read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Reading an unread room after a redaction of the latest message makes it read", () => {
|
|
||||||
// Given an unread room
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// And the latest message has been redacted
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
|
|
||||||
// When I read the room
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// Then it becomes read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Reading an unread room after a redaction of an older message makes it read", () => {
|
|
||||||
// Given an unread room with an earlier redaction
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg1")]);
|
|
||||||
|
|
||||||
// When I read the room
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// Then it becomes read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Marking an unread room as read after a redaction makes it read", () => {
|
|
||||||
// Given an unread room where latest message is redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I mark it as read
|
|
||||||
markAsRead(room2);
|
|
||||||
|
|
||||||
// Then it becomes read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Sending and redacting a message after marking the room as read makes it read", () => {
|
|
||||||
// Given a room that is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When a message is sent and then redacted
|
|
||||||
receiveMessages(room2, ["Msg3"]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// Then the room is read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Redacting a message after marking the room as read leaves it read", () => {
|
|
||||||
// Given a room that is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When we redact some messages
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg1")]);
|
|
||||||
|
|
||||||
// Then it is still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Redacting one of the unread messages reduces the unread count", () => {
|
|
||||||
// Given an unread room
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
|
|
||||||
// When I redact a non-latest message
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
|
|
||||||
// Then the unread count goes down
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// And when I redact the latest message
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// Then the unread count goes down again
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Redacting one of the unread messages reduces the unread count after restart", () => {
|
|
||||||
// Given unread count was reduced by redacting messages
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the unread count is still reduced
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Redacting all unread messages makes the room read", () => {
|
|
||||||
// Given an unread room
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I redact all the unread messages
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg1")]);
|
|
||||||
|
|
||||||
// Then the room is back to being read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because it flakes saying the room is unread when it should be read
|
|
||||||
it.skip("Redacting all unread messages makes the room read after restart", () => {
|
|
||||||
// Given all unread messages were redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg1")]);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Reacting to a redacted message leaves the room read", () => {
|
|
||||||
// Given a redacted message exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// And the room is read
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
cy.wait(200);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When I react to the redacted message
|
|
||||||
receiveMessages(room2, [reactionTo("Msg2", "🪿")]);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
it("Editing a redacted message leaves the room read", () => {
|
|
||||||
// Given a redacted message exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// And the room is read
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When I attempt to edit the redacted message
|
|
||||||
receiveMessages(room2, [editOf("Msg2", "Msg2 is BACK")]);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because flakes showing 2 unread instead of 1
|
|
||||||
it.skip("A reply to a redacted message makes the room unread", () => {
|
|
||||||
// Given a message was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// And the room is read
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When I receive a reply to the redacted message
|
|
||||||
receiveMessages(room2, [replyTo("Msg2", "Reply to Msg2")]);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Reading a reply to a redacted message marks the room as read", () => {
|
|
||||||
// Given someone replied to a redacted message
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Msg1", "Msg2"]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [replyTo("Msg2", "Reply to Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I read the reply
|
|
||||||
goTo(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
goTo(room1);
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("in threads", () => {
|
|
||||||
// XXX: fails because it flakes saying the room is unread when it should be read
|
|
||||||
it.skip("Redacting the threaded message pointed to by my receipt leaves the room read", () => {
|
|
||||||
// Given I have some threads
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "ThreadMsg1"),
|
|
||||||
threadedOff("Root", "ThreadMsg2"),
|
|
||||||
"Root2",
|
|
||||||
threadedOff("Root2", "Root2->A"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 5);
|
|
||||||
|
|
||||||
// And I have read them
|
|
||||||
goTo(room2);
|
|
||||||
assertUnreadThread("Root");
|
|
||||||
openThread("Root");
|
|
||||||
assertUnreadLessThan(room2, 4);
|
|
||||||
openThread("Root2");
|
|
||||||
assertRead(room2);
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When the latest message in a thread is redacted
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg2")]);
|
|
||||||
|
|
||||||
// Then the room and thread are still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because it flakes (on CI only)
|
|
||||||
it.skip("Reading an unread thread after a redaction of the latest message makes it read", () => {
|
|
||||||
// Given an unread thread where the latest message was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnreadThread("Root");
|
|
||||||
|
|
||||||
// When I read the thread
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// Then the thread is read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because the unread count is still 1 when it should be 0
|
|
||||||
it.skip("Reading an unread thread after a redaction of the latest message makes it read after restart", () => {
|
|
||||||
// Given a redacted message is not counted in the unread count
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnreadThread("Root");
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because it flakes (on CI only)
|
|
||||||
it.skip("Reading an unread thread after a redaction of an older message makes it read", () => {
|
|
||||||
// Given an unread thread where an older message was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnreadThread("Root");
|
|
||||||
|
|
||||||
// When I read the thread
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// Then the thread is read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because it flakes (on CI only)
|
|
||||||
it.skip("Marking an unread thread as read after a redaction makes it read", () => {
|
|
||||||
// Given an unread thread where an older message was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg1")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When I mark the room as read
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// Then the thread is read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because the room has an unread dot after I marked it as read
|
|
||||||
it.skip("Sending and redacting a message after marking the thread as read leaves it read", () => {
|
|
||||||
// Given a thread exists and is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I send and redact a message
|
|
||||||
receiveMessages(room2, [threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// Then the room and thread are read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because the room has an unread dot after I marked it as read
|
|
||||||
it.skip("Redacting a message after marking the thread as read leaves it read", () => {
|
|
||||||
// Given a thread exists and is marked as read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
markAsRead(room2);
|
|
||||||
assertRead(room2);
|
|
||||||
|
|
||||||
// When I redact a message
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg1")]);
|
|
||||||
|
|
||||||
// Then the room and thread are read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because it flakes - sometimes the room is still unread after opening the thread (initially)
|
|
||||||
it.skip("Reacting to a redacted message leaves the thread read", () => {
|
|
||||||
// Given a message in a thread was redacted and everything is read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When we receive a reaction to the redacted event
|
|
||||||
receiveMessages(room2, [reactionTo("Msg2", "z")]);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because the room is still unread after opening the thread (initially)
|
|
||||||
it.skip("Editing a redacted message leaves the thread read", () => {
|
|
||||||
// Given a message in a thread was redacted and everything is read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When we receive an edit of the redacted message
|
|
||||||
receiveMessages(room2, [editOf("Msg2", "New Msg2")]);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: failed because flakes: https://github.com/vector-im/element-web/issues/26594
|
|
||||||
it.skip("Reading a thread after a reaction to a redacted message marks the thread as read", () => {
|
|
||||||
// Given a redacted message in a thread exists, but someone reacted to it before it was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "Msg2"),
|
|
||||||
threadedOff("Root", "Msg3"),
|
|
||||||
reactionTo("Msg3", "x"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// When we read the thread
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
|
|
||||||
// Then the thread (and room) are read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because the unread count stays at 1 instead of zero
|
|
||||||
it.skip("Reading a thread containing a redacted, edited message marks the thread as read", () => {
|
|
||||||
// Given a redacted message in a thread exists, but someone edited it before it was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "Msg2"),
|
|
||||||
threadedOff("Root", "Msg3"),
|
|
||||||
editOf("Msg3", "Msg3 Edited"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// When we read the thread
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
|
|
||||||
// Then the thread (and room) are read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because the read count drops to 1 but not to zero (this is a genuine stuck unread case)
|
|
||||||
it.skip("Reading a reply to a redacted message marks the thread as read", () => {
|
|
||||||
// Given a redacted message in a thread exists, but someone replied before it was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "Msg2"),
|
|
||||||
threadedOff("Root", "Msg3"),
|
|
||||||
replyTo("Msg3", "Msg3Reply"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 4);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// When we read the thread, creating a receipt that points at the edit
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
|
|
||||||
// Then the thread (and room) are read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because flakes saying 2 unread instead of 1
|
|
||||||
it.skip("Reading a thread root when its only message has been redacted leaves the room read", () => {
|
|
||||||
// Given we had a thread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2")]);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// And then redacted the message that makes it a thread
|
|
||||||
receiveMessages(room2, [redactionOf("Msg2")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When we read the main timeline
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then the room is read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails because flakes with matrix-js-sdk#3798 (only when all other tests are enabled!)
|
|
||||||
it.skip("A thread with a redacted unread is still read after restart", () => {
|
|
||||||
// Given I sent and redacted a message in an otherwise-read thread
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "ThreadMsg1"), threadedOff("Root", "ThreadMsg2")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
receiveMessages(room2, [threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
goTo(room1);
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room and thread are still read
|
|
||||||
assertRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails because it flakes
|
|
||||||
it.skip("A thread with a read redaction is still read after restart", () => {
|
|
||||||
// Given my receipt points at a redacted thread message
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "ThreadMsg1"),
|
|
||||||
threadedOff("Root", "ThreadMsg2"),
|
|
||||||
"Root2",
|
|
||||||
threadedOff("Root2", "Root2->A"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 5);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnreadThread("Root");
|
|
||||||
openThread("Root");
|
|
||||||
assertUnreadLessThan(room2, 4);
|
|
||||||
openThread("Root2");
|
|
||||||
assertRead(room2);
|
|
||||||
closeThreadsPanel();
|
|
||||||
goTo(room1);
|
|
||||||
assertRead(room2);
|
|
||||||
receiveMessages(room2, [redactionOf("ThreadMsg2")]);
|
|
||||||
assertStillRead(room2);
|
|
||||||
goTo(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
// XXX: fails for the same reason as "Reading a reply to a redacted message marks the thread as read"
|
|
||||||
it.skip("A thread with an unread reply to a redacted message is still unread after restart", () => {
|
|
||||||
// Given a redacted message in a thread exists, but someone replied before it was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "Msg2"),
|
|
||||||
threadedOff("Root", "Msg3"),
|
|
||||||
replyTo("Msg3", "Msg3Reply"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 4);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// And we have read all this
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// XXX: fails for the same reason as "Reading a reply to a redacted message marks the thread as read
|
|
||||||
it.skip("A thread with a read reply to a redacted message is still read after restart", () => {
|
|
||||||
// Given a redacted message in a thread exists, but someone replied before it was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, [
|
|
||||||
"Root",
|
|
||||||
threadedOff("Root", "Msg2"),
|
|
||||||
threadedOff("Root", "Msg3"),
|
|
||||||
replyTo("Msg3", "Msg3Reply"),
|
|
||||||
]);
|
|
||||||
assertUnread(room2, 4);
|
|
||||||
receiveMessages(room2, [redactionOf("Msg3")]);
|
|
||||||
|
|
||||||
// And I read it, so the room is read
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
|
|
||||||
// When I restart
|
|
||||||
saveAndReload();
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("thread roots", () => {
|
|
||||||
it("Redacting a thread root after it was read leaves the room read", () => {
|
|
||||||
// Given a thread exists and is read
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
|
|
||||||
// When someone redacts the thread root
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertStillRead(room2);
|
|
||||||
});
|
|
||||||
// TODO: Can't open a thread on a redacted thread root
|
|
||||||
it.skip("Redacting a thread root still allows us to read the thread", () => {
|
|
||||||
// Given an unread thread exists
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
|
|
||||||
// When someone redacts the thread root
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
|
|
||||||
// Then the room is still unread
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
|
|
||||||
// And I can open the thread and read it
|
|
||||||
goTo(room2);
|
|
||||||
assertUnread(room2, 2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
});
|
|
||||||
// TODO: Can't open a thread on a redacted thread root
|
|
||||||
it.skip("Sending a threaded message onto a redacted thread root leaves the room unread", () => {
|
|
||||||
// Given a thread exists, is read and its root is redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
|
|
||||||
// When we receive a new message on it
|
|
||||||
receiveMessages(room2, [threadedOff("Root", "Msg4")]);
|
|
||||||
|
|
||||||
// Then the room and thread are unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
goTo(room2);
|
|
||||||
assertUnreadThread("Root");
|
|
||||||
});
|
|
||||||
it("Reacting to a redacted thread root leaves the room read", () => {
|
|
||||||
// Given a thread exists, is read and the root was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
|
|
||||||
// When I react to the old root
|
|
||||||
receiveMessages(room2, [reactionTo("Root", "y")]);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Editing a redacted thread root leaves the room read", () => {
|
|
||||||
// Given a thread exists, is read and the root was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
|
|
||||||
// When I edit the old root
|
|
||||||
receiveMessages(room2, [editOf("Root", "New Root")]);
|
|
||||||
|
|
||||||
// Then the room is still read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
it("Replying to a redacted thread root makes the room unread", () => {
|
|
||||||
// Given a thread exists, is read and the root was redacted
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
|
|
||||||
// When I reply to the old root
|
|
||||||
receiveMessages(room2, [replyTo("Root", "Reply!")]);
|
|
||||||
|
|
||||||
// Then the room is unread
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
});
|
|
||||||
it("Reading a reply to a redacted thread root makes the room read", () => {
|
|
||||||
// Given a thread exists, is read and the root was redacted, and
|
|
||||||
// someone replied to it
|
|
||||||
goTo(room1);
|
|
||||||
receiveMessages(room2, ["Root", threadedOff("Root", "Msg2"), threadedOff("Root", "Msg3")]);
|
|
||||||
assertUnread(room2, 3);
|
|
||||||
goTo(room2);
|
|
||||||
openThread("Root");
|
|
||||||
assertRead(room2);
|
|
||||||
assertReadThread("Root");
|
|
||||||
receiveMessages(room2, [redactionOf("Root")]);
|
|
||||||
assertStillRead(room2);
|
|
||||||
receiveMessages(room2, [replyTo("Root", "Reply!")]);
|
|
||||||
assertUnread(room2, 1);
|
|
||||||
|
|
||||||
// When I read the room
|
|
||||||
goTo(room2);
|
|
||||||
|
|
||||||
// Then it becomes read
|
|
||||||
assertRead(room2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
492
playwright/e2e/read-receipts/editing-messages.spec.ts
Normal file
492
playwright/e2e/read-receipts/editing-messages.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
466
playwright/e2e/read-receipts/high-level.spec.ts
Normal file
466
playwright/e2e/read-receipts/high-level.spec.ts
Normal 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", () => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
591
playwright/e2e/read-receipts/index.ts
Normal file
591
playwright/e2e/read-receipts/index.ts
Normal 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 };
|
59
playwright/e2e/read-receipts/missing-referents.spec.ts
Normal file
59
playwright/e2e/read-receipts/missing-referents.spec.ts
Normal 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?
|
||||||
|
});
|
||||||
|
});
|
574
playwright/e2e/read-receipts/new-messages.spec.ts
Normal file
574
playwright/e2e/read-receipts/new-messages.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
359
playwright/e2e/read-receipts/reactions.spec.ts
Normal file
359
playwright/e2e/read-receipts/reactions.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,7 +4,7 @@ Tips for writing these tests:
|
||||||
|
|
||||||
- Break up your tests into the smallest test case possible. The purpose of
|
- 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.
|
these tests is to understand hard-to-find bugs, so small tests are necessary.
|
||||||
We know that Cypress recommends combining tests together for performance, but
|
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
|
that will frustrate our goals here. (We will need to find a different way to
|
||||||
reduce CI time.)
|
reduce CI time.)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ Tips for writing these tests:
|
||||||
markAsRead(room2);
|
markAsRead(room2);
|
||||||
assertRead(room2);
|
assertRead(room2);
|
||||||
You should especially follow this rule if you are jumping to a different
|
You should especially follow this rule if you are jumping to a different
|
||||||
room or similar straight afterwards.
|
room or similar straight afterward.
|
||||||
|
|
||||||
- Use assertStillRead() if you are asserting something is read when it was
|
- 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
|
also read before. This waits a little while to make sure you're not getting a
|
1038
playwright/e2e/read-receipts/redactions.spec.ts
Normal file
1038
playwright/e2e/read-receipts/redactions.spec.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -52,6 +52,19 @@ export class Client {
|
||||||
return this.client.evaluate(fn, arg);
|
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 roomId ID of the room to send the event into
|
||||||
* @param threadId ID of the thread to send into or null for main timeline
|
* @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.
|
* Create a room with given options.
|
||||||
* @param options the options to apply when creating the room
|
* @param options the options to apply when creating the room
|
||||||
|
|
Loading…
Reference in a new issue