Migrate read-receipts.spec.ts from Cypress to Playwright (#11995)

* Migrate read-receipts.spec.ts from Cypress to Playwright

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

* Update client.ts

* Serialise test message sending

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28788/head^2
Michael Telatynski 2023-12-05 13:24:10 +00:00 committed by GitHub
parent 447d2ce415
commit 99b580d501
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 359 additions and 374 deletions

View File

@ -1,355 +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.
*/
/// <reference types="cypress" />
import type { MatrixClient, MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("Read receipts", () => {
const userName = "Mae";
const botName = "Other User";
const selectedRoomName = "Selected Room";
const otherRoomName = "Other Room";
let homeserver: HomeserverInstance;
let otherRoomId: string;
let selectedRoomId: string;
let bot: MatrixClient | undefined;
const botSendMessage = (no = 1): Cypress.Chainable<ISendEventResponse> => {
return cy.botSendMessage(bot, otherRoomId, `Message ${no}`);
};
const botSendThreadMessage = (threadId: string): Cypress.Chainable<ISendEventResponse> => {
return cy.botSendThreadMessage(bot, otherRoomId, threadId, "Message");
};
const fakeEventFromSent = (eventResponse: ISendEventResponse, threadRootId: string | undefined): MatrixEvent => {
return {
getRoomId: () => otherRoomId,
getId: () => eventResponse.event_id,
threadRootId,
getTs: () => 1,
isRelation: (relType) => {
return !relType || relType === "m.thread";
},
} as any as MatrixEvent;
};
/**
* 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 = (
eventResponse: ISendEventResponse,
threadRootEventResponse: ISendEventResponse = undefined,
) => {
cy.sendReadReceipt(fakeEventFromSent(eventResponse, threadRootEventResponse?.event_id));
};
/**
* Send an unthreaded receipt marking the message referred to in
* eventResponse as read.
*/
const sendUnthreadedReadReceipt = (eventResponse: ISendEventResponse) => {
cy.sendReadReceipt(fakeEventFromSent(eventResponse, undefined), "m.read" as any as ReceiptType, true);
};
beforeEach(() => {
/*
* 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.
*/
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, userName)
.then(() => {
cy.createRoom({ name: selectedRoomName }).then((createdRoomId) => {
selectedRoomId = createdRoomId;
});
})
.then(() => {
cy.createRoom({ name: otherRoomName }).then((createdRoomId) => {
otherRoomId = createdRoomId;
});
})
.then(() => {
cy.getBot(homeserver, { displayName: botName }).then((botClient) => {
bot = botClient;
});
})
.then(() => {
// Invite the bot to Other room
cy.inviteUser(otherRoomId, bot.getUserId());
cy.visit("/#/room/" + otherRoomId);
cy.findByText(botName + " joined the room").should("exist");
// Then go into Selected room
cy.visit("/#/room/" + selectedRoomId);
});
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it(
"With sync accumulator, considers main thread and unthreaded receipts #24629",
{
// When #24629 exists, the test fails the first time but passes later, so we disable retries
// to be sure we are going to fail if the bug comes back.
// Why does it pass the second time? I wish I knew. (andyb)
retries: 0,
},
() => {
// 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
botSendMessage();
botSendMessage().then((main2) => {
botSendMessage().then((main3) => {
// (So the room starts off unread)
cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
// When we send a threaded receipt for the last event in main
// And an unthreaded receipt for an earlier event
sendThreadedReadReceipt(main3);
sendUnthreadedReadReceipt(main2);
// (So the room has no unreads)
cy.findByLabelText(`${otherRoomName}`).should("exist");
// And we persuade the app to persist its state to indexeddb by reloading and waiting
cy.reload();
cy.findByLabelText(`${selectedRoomName}`).should("exist");
// And we reload again, fetching the persisted state FROM indexeddb
cy.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.)
cy.findByLabelText(`${otherRoomName}`).should("exist");
cy.findByLabelText(`${otherRoomName} Unread messages.`).should("not.exist");
});
});
},
);
it("Recognises unread messages on main thread after receiving a receipt for earlier ones", () => {
// Given we sent 3 events on the main thread
botSendMessage();
botSendMessage().then((main2) => {
botSendMessage().then(() => {
// (The room starts off unread)
cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
// When we send a threaded receipt for the second-last event in main
sendThreadedReadReceipt(main2);
// Then the room has only one unread
cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
});
});
});
it("Considers room read if there is only a main thread and we have a main receipt", () => {
// Given we sent 3 events on the main thread
botSendMessage();
botSendMessage().then(() => {
botSendMessage().then((main3) => {
// (The room starts off unread)
cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
// When we send a threaded receipt for the last event in main
sendThreadedReadReceipt(main3);
// Then the room has no unreads
cy.findByLabelText(`${otherRoomName}`).should("exist");
});
});
});
it("Recognises unread messages on other thread after receiving a receipt for earlier ones", () => {
// Given we sent 3 events on the main thread
botSendMessage().then((main1) => {
botSendThreadMessage(main1.event_id).then((thread1a) => {
botSendThreadMessage(main1.event_id).then((thread1b) => {
// 1 unread on the main thread, 2 in the new thread
cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
// When we send receipts for main, and the second-last in the thread
sendThreadedReadReceipt(main1);
sendThreadedReadReceipt(thread1a, main1);
// Then the room has only one unread - the one in the thread
cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
});
});
});
});
it("Considers room read if there are receipts for main and other thread", () => {
// Given we sent 3 events on the main thread
botSendMessage().then((main1) => {
botSendThreadMessage(main1.event_id).then((thread1a) => {
botSendThreadMessage(main1.event_id).then((thread1b) => {
// 1 unread on the main thread, 2 in the new thread
cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
// When we send receipts for main, and the last in the thread
sendThreadedReadReceipt(main1);
sendThreadedReadReceipt(thread1b, main1);
// Then the room has no unreads
cy.findByLabelText(`${otherRoomName}`).should("exist");
});
});
});
});
it("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", () => {
// Given we sent 3 events on the main thread
botSendMessage().then((main1) => {
botSendThreadMessage(main1.event_id).then((thread1a) => {
botSendThreadMessage(main1.event_id).then(() => {
// 1 unread on the main thread, 2 in the new thread
cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
// When we send an unthreaded receipt for the second-last in the thread
sendUnthreadedReadReceipt(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.
cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
});
});
});
});
it("Recognises unread messages on main after receiving a unthreaded receipt for a thread message", () => {
// Given we sent 3 events on the main thread
botSendMessage().then((main1) => {
botSendThreadMessage(main1.event_id).then(() => {
botSendThreadMessage(main1.event_id).then((thread1b) => {
botSendMessage().then(() => {
// 2 unreads on the main thread, 2 in the new thread
cy.findByLabelText(`${otherRoomName} 4 unread messages.`).should("exist");
// When we send an unthreaded receipt for the last in the thread
sendUnthreadedReadReceipt(thread1b);
// Then the room has only one unread - the one in the
// main thread, because it is later than the unthreaded
// receipt.
cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
});
});
});
});
});
/**
* 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
*/
it("Should send the correct receipts", () => {
const uriEncodedOtherRoomId = encodeURIComponent(otherRoomId);
cy.intercept({
method: "POST",
url: new RegExp(
`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/receipt/m\\.read/.+`,
),
}).as("receiptRequest");
const numberOfMessages = 20;
const sendMessagePromises = [];
for (let i = 1; i <= numberOfMessages; i++) {
sendMessagePromises.push(botSendMessage(i));
}
cy.all(sendMessagePromises).then((sendMessageResponses) => {
const lastMessageId = sendMessageResponses.at(-1).event_id;
const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
// wait until all messages have been received
cy.findByLabelText(`${otherRoomName} ${sendMessagePromises.length} unread messages.`).should("exist");
// switch to the room with the messages
cy.visit("/#/room/" + otherRoomId);
cy.wait("@receiptRequest").should((req) => {
// assert the read receipt for the last message in the room
expect(req.request.url).to.contain(uriEncodedLastMessageId);
expect(req.request.body).to.deep.equal({
thread_id: "main",
});
});
// the following code tests the fully read marker somewhere in the middle of the room
cy.intercept({
method: "POST",
url: new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/read_markers`),
}).as("readMarkersRequest");
cy.findByRole("button", { name: "Jump to first unread message." }).click();
cy.wait("@readMarkersRequest").should((req) => {
// since this is not pixel perfect,
// the fully read marker should be +/- 1 around the last visible message
expect(Array.from(Object.keys(req.request.body))).to.deep.equal(["m.fully_read"]);
expect(req.request.body["m.fully_read"]).to.be.oneOf([
sendMessageResponses[11].event_id,
sendMessageResponses[12].event_id,
sendMessageResponses[13].event_id,
]);
});
// the following code tests the fully read marker at the bottom of the room
cy.intercept({
method: "POST",
url: new RegExp(`http://localhost:\\d+/_matrix/client/v3/rooms/${uriEncodedOtherRoomId}/read_markers`),
}).as("readMarkersRequest");
cy.findByRole("button", { name: "Scroll to most recent messages" }).click();
cy.wait("@readMarkersRequest").should((req) => {
expect(req.request.body).to.deep.equal({
["m.fully_read"]: sendMessageResponses.at(-1).event_id,
});
});
});
});
});

View File

@ -19,14 +19,12 @@ limitations under the License.
import type {
MatrixClient,
Room,
MatrixEvent,
IContent,
FileType,
Upload,
UploadOpts,
ICreateRoomOpts,
ISendEventResponse,
ReceiptType,
} from "matrix-js-sdk/src/matrix";
import Chainable = Cypress.Chainable;
import { UserCredentials } from "./login";
@ -76,13 +74,6 @@ declare global {
eventType: string,
content: IContent,
): Chainable<ISendEventResponse>;
/**
* @param {MatrixEvent} event
* @param {ReceiptType} receiptType
* @param {boolean} unthreaded
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
sendReadReceipt(event: MatrixEvent, receiptType?: ReceiptType, unthreaded?: boolean): Chainable<{}>;
/**
* @param {string} name
* @param {module:client.callback} callback Optional.
@ -209,15 +200,6 @@ Cypress.Commands.add(
},
);
Cypress.Commands.add(
"sendReadReceipt",
(event: MatrixEvent, receiptType?: ReceiptType, unthreaded?: boolean): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.sendReadReceipt(event, receiptType, unthreaded);
});
},
);
Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.setDisplayName(name);

View File

@ -0,0 +1,331 @@
/*
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 { test, expect } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
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}`);
});
test("With sync accumulator, considers main thread and unthreaded receipts #24629", async ({ page, app, bot }) => {
// Details are in https://github.com/vector-im/element-web/issues/24629
// This proves we've fixed one of the "stuck unreads" issues.
// Given we sent 3 events on the main thread
await sendMessage(bot);
const main2 = await sendMessage(bot);
const main3 = await sendMessage(bot);
// (So the room starts off unread)
await expect(page.getByLabel(`${otherRoomName} 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,
}) => {
// 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
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).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 expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
});
test("Considers room read if there are receipts for main and other thread", 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);
// 1 unread on the main thread, 2 in the new thread
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).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();
});
test("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", async ({
page,
app,
bot,
}) => {
// 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
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).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.
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
});
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
await expect(page.getByLabel(`${otherRoomName} 4 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,
});
});
});

View File

@ -17,7 +17,15 @@ limitations under the License.
import { JSHandle, Page } from "@playwright/test";
import { PageFunctionOn } from "playwright-core/types/structs";
import type { IContent, ICreateRoomOpts, ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import type {
IContent,
ICreateRoomOpts,
ISendEventResponse,
MatrixClient,
Room,
MatrixEvent,
ReceiptType,
} from "matrix-js-sdk/src/matrix";
export class Client {
protected client: JSHandle<MatrixClient>;
@ -196,4 +204,23 @@ export class Client {
userId,
});
}
/**
* @param {MatrixEvent} event
* @param {ReceiptType} receiptType
* @param {boolean} unthreaded
*/
public async sendReadReceipt(
event: JSHandle<MatrixEvent>,
receiptType?: ReceiptType,
unthreaded?: boolean,
): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(
(client, { event, receiptType, unthreaded }) => {
return client.sendReadReceipt(event, receiptType, unthreaded);
},
{ event, receiptType, unthreaded },
);
}
}