element-web/playwright/e2e/read-receipts/read-receipts.spec.ts

346 lines
14 KiB
TypeScript

/*
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 } from "@playwright/test";
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
import { expect } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
import { test } from ".";
test.describe("Read receipts", () => {
test.use({
displayName: "Mae",
botCreateOpts: { displayName: "Other User" },
});
const selectedRoomName = "Selected Room";
const otherRoomName = "Other Room";
let otherRoomId: string;
let selectedRoomId: string;
const sendMessage = async (bot: Bot, no = 1): Promise<ISendEventResponse> => {
return bot.sendMessage(otherRoomId, { body: `Message ${no}`, msgtype: "m.text" });
};
const botSendThreadMessage = (bot: Bot, threadId: string): Promise<ISendEventResponse> => {
return bot.sendEvent(otherRoomId, threadId, "m.room.message", { body: "Message", msgtype: "m.text" });
};
const fakeEventFromSent = (
app: ElementAppPage,
eventResponse: ISendEventResponse,
threadRootId: string | undefined,
): Promise<JSHandle<MatrixEvent>> => {
return app.client.evaluateHandle(
(client, { otherRoomId, eventResponse, threadRootId }) => {
return {
getRoomId: () => otherRoomId,
getId: () => eventResponse.event_id,
threadRootId,
getTs: () => 1,
isRelation: (relType) => {
return !relType || relType === "m.thread";
},
} as any as MatrixEvent;
},
{ otherRoomId, eventResponse, threadRootId },
);
};
/**
* Send a threaded receipt marking the message referred to in
* eventResponse as read. If threadRootEventResponse is supplied, the
* receipt will have its event_id as the thread root ID for the receipt.
*/
const sendThreadedReadReceipt = async (
app: ElementAppPage,
eventResponse: ISendEventResponse,
threadRootEventResponse: ISendEventResponse = undefined,
) => {
await app.client.sendReadReceipt(
await fakeEventFromSent(app, eventResponse, threadRootEventResponse?.event_id),
);
};
/**
* Send an unthreaded receipt marking the message referred to in
* eventResponse as read.
*/
const sendUnthreadedReadReceipt = async (app: ElementAppPage, eventResponse: ISendEventResponse) => {
await app.client.sendReadReceipt(
await fakeEventFromSent(app, eventResponse, undefined),
"m.read" as any as ReceiptType,
true,
);
};
test.beforeEach(async ({ page, app, user, bot }) => {
/*
* Create 2 rooms:
*
* - Selected room - this one is clicked in the UI
* - Other room - this one contains the bot, which will send events so
* we can check its unread state.
*/
selectedRoomId = await app.client.createRoom({ name: selectedRoomName });
// Invite the bot to Other room
otherRoomId = await app.client.createRoom({ name: otherRoomName, invite: [bot.credentials.userId] });
await page.goto(`/#/room/${otherRoomId}`);
await expect(page.getByText(`${bot.credentials.displayName} joined the room`)).toBeVisible();
// Then go into Selected room
await page.goto(`/#/room/${selectedRoomId}`);
});
// Disabled due to flakiness: https://github.com/element-hq/element-web/issues/26895
test.skip("With sync accumulator, considers main thread and unthreaded receipts #24629", async ({
page,
app,
bot,
}) => {
// Details are in https://github.com/vector-im/element-web/issues/24629
// This proves we've fixed one of the "stuck unreads" issues.
// Given we sent 3 events on the main thread
await sendMessage(bot);
const main2 = await sendMessage(bot);
const main3 = await sendMessage(bot);
// (So the room starts off unread)
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the last event in main
// And an unthreaded receipt for an earlier event
await sendThreadedReadReceipt(app, main3);
await sendUnthreadedReadReceipt(app, main2);
// (So the room has no unreads)
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
// And we persuade the app to persist its state to indexeddb by reloading and waiting
await page.reload();
await expect(page.getByLabel(`${selectedRoomName}`)).toBeVisible();
// And we reload again, fetching the persisted state FROM indexeddb
await page.reload();
// Then the room is read, because the persisted state correctly remembers both
// receipts. (In #24629, the unthreaded receipt overwrote the main thread one,
// meaning that the room still said it had unread messages.)
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
await expect(page.getByLabel(`${otherRoomName} Unread messages.`)).not.toBeVisible();
});
test("Recognises unread messages on main thread after receiving a receipt for earlier ones", async ({
page,
app,
bot,
}) => {
// Given we sent 3 events on the main thread
await sendMessage(bot);
const main2 = await sendMessage(bot);
await sendMessage(bot);
// (The room starts off unread)
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the second-last event in main
await sendThreadedReadReceipt(app, main2);
// Then the room has only one unread
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
});
test("Considers room read if there is only a main thread and we have a main receipt", async ({
page,
app,
bot,
}) => {
// Given we sent 3 events on the main thread
await sendMessage(bot);
await sendMessage(bot);
const main3 = await sendMessage(bot);
// (The room starts off unread)
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the last event in main
await sendThreadedReadReceipt(app, main3);
// Then the room has no unreads
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
});
test("Recognises unread messages on other thread after receiving a receipt for earlier ones", async ({
page,
app,
bot,
util,
}) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
const thread1a = await botSendThreadMessage(bot, main1.event_id);
await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread that aren't shown
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
// When we send receipts for main, and the second-last in the thread
await sendThreadedReadReceipt(app, main1);
await sendThreadedReadReceipt(app, thread1a, main1);
// Then the room has only one unread - the one in the thread
await util.goTo(otherRoomName);
await util.assertUnreadThread("Message 1");
});
test("Considers room read if there are receipts for main and other thread", async ({ page, app, bot, util }) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
await botSendThreadMessage(bot, main1.event_id);
const thread1b = await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread which don't show
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
// When we send receipts for main, and the last in the thread
await sendThreadedReadReceipt(app, main1);
await sendThreadedReadReceipt(app, thread1b, main1);
// Then the room has no unreads
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
await util.goTo(otherRoomName);
await util.assertReadThread("Message 1");
});
test("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", async ({
page,
app,
bot,
util,
}) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
const thread1a = await botSendThreadMessage(bot, main1.event_id);
await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread which don't count
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
// When we send an unthreaded receipt for the second-last in the thread
await sendUnthreadedReadReceipt(app, thread1a);
// Then the room has only one unread - the one in the
// thread. The one in main is read because the unthreaded
// receipt is for a later event. The room should therefore be
// read, and the thread unread.
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
await util.goTo(otherRoomName);
await util.assertUnreadThread("Message 1");
});
test("Recognises unread messages on main after receiving a unthreaded receipt for a thread message", async ({
page,
app,
bot,
}) => {
// Given we sent 3 events on the main thread
const main1 = await sendMessage(bot);
await botSendThreadMessage(bot, main1.event_id);
const thread1b = await botSendThreadMessage(bot, main1.event_id);
await sendMessage(bot);
// 2 unreads on the main thread, 2 in the new thread which don't count
await expect(page.getByLabel(`${otherRoomName} 2 unread messages.`)).toBeVisible();
// When we send an unthreaded receipt for the last in the thread
await sendUnthreadedReadReceipt(app, thread1b);
// Then the room has only one unread - the one in the
// main thread, because it is later than the unthreaded
// receipt.
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
});
/**
* The idea of this test is to intercept the receipt / read read_markers requests and
* assert that the correct ones are sent.
* Prose playbook:
* - Another user sends enough messages that the timeline becomes scrollable
* - The current user looks at the room and jumps directly to the first unread message
* - At this point, a receipt for the last message in the room and
* a fully read marker for the last visible message are expected to be sent
* - Then the user jumps to the end of the timeline
* - A fully read marker for the last message in the room is expected to be sent
*/
test("Should send the correct receipts", async ({ page, bot }) => {
const uriEncodedOtherRoomId = encodeURIComponent(otherRoomId);
const receiptRequestPromise = page.waitForRequest(
new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/receipt/m\\.read/.+`),
);
const numberOfMessages = 20;
const sendMessageResponses: ISendEventResponse[] = [];
for (let i = 1; i <= numberOfMessages; i++) {
sendMessageResponses.push(await sendMessage(bot, i));
}
const lastMessageId = sendMessageResponses.at(-1).event_id;
const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
// wait until all messages have been received
await expect(page.getByLabel(`${otherRoomName} ${sendMessageResponses.length} unread messages.`)).toBeVisible();
// switch to the room with the messages
await page.goto(`/#/room/${otherRoomId}`);
const receiptRequest = await receiptRequestPromise;
// assert the read receipt for the last message in the room
expect(receiptRequest.url()).toContain(uriEncodedLastMessageId);
expect(receiptRequest.postDataJSON()).toEqual({
thread_id: "main",
});
// the following code tests the fully read marker somewhere in the middle of the room
const readMarkersRequestPromise = page.waitForRequest(
new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/read_markers`),
);
await page.getByRole("button", { name: "Jump to first unread message." }).click();
const readMarkersRequest = await readMarkersRequestPromise;
// since this is not pixel perfect,
// the fully read marker should be +/- 1 around the last visible message
expect([
sendMessageResponses[11].event_id,
sendMessageResponses[12].event_id,
sendMessageResponses[13].event_id,
]).toContain(readMarkersRequest.postDataJSON()["m.fully_read"]);
// the following code tests the fully read marker at the bottom of the room
const readMarkersRequestPromise2 = page.waitForRequest(
new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/read_markers`),
);
await page.getByRole("button", { name: "Scroll to most recent messages" }).click();
const readMarkersRequest2 = await readMarkersRequestPromise2;
expect(readMarkersRequest2.postDataJSON()).toEqual({
["m.fully_read"]: sendMessageResponses.at(-1).event_id,
});
});
});