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>
This commit is contained in:
parent
447d2ce415
commit
99b580d501
4 changed files with 359 additions and 374 deletions
|
@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -19,14 +19,12 @@ limitations under the License.
|
||||||
import type {
|
import type {
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
Room,
|
Room,
|
||||||
MatrixEvent,
|
|
||||||
IContent,
|
IContent,
|
||||||
FileType,
|
FileType,
|
||||||
Upload,
|
Upload,
|
||||||
UploadOpts,
|
UploadOpts,
|
||||||
ICreateRoomOpts,
|
ICreateRoomOpts,
|
||||||
ISendEventResponse,
|
ISendEventResponse,
|
||||||
ReceiptType,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import { UserCredentials } from "./login";
|
import { UserCredentials } from "./login";
|
||||||
|
@ -76,13 +74,6 @@ declare global {
|
||||||
eventType: string,
|
eventType: string,
|
||||||
content: IContent,
|
content: IContent,
|
||||||
): Chainable<ISendEventResponse>;
|
): 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 {string} name
|
||||||
* @param {module:client.callback} callback Optional.
|
* @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<{}> => {
|
Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
|
||||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||||
return cli.setDisplayName(name);
|
return cli.setDisplayName(name);
|
||||||
|
|
331
playwright/e2e/read-receipts/read-receipts.spec.ts
Normal file
331
playwright/e2e/read-receipts/read-receipts.spec.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -17,7 +17,15 @@ limitations under the License.
|
||||||
import { JSHandle, Page } from "@playwright/test";
|
import { JSHandle, Page } from "@playwright/test";
|
||||||
import { PageFunctionOn } from "playwright-core/types/structs";
|
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 {
|
export class Client {
|
||||||
protected client: JSHandle<MatrixClient>;
|
protected client: JSHandle<MatrixClient>;
|
||||||
|
@ -196,4 +204,23 @@ export class Client {
|
||||||
userId,
|
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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue