diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts index f8d7dd1e3f..ae334ecf92 100644 --- a/cypress/e2e/editing/editing.spec.ts +++ b/cypress/e2e/editing/editing.spec.ts @@ -16,9 +16,9 @@ limitations under the License. /// -import type { MsgType } from "matrix-js-sdk/src/@types/event"; +import type { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; -import type { EventType } from "matrix-js-sdk/src/@types/event"; +import type { IContent } from "matrix-js-sdk/src/models/event"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import Chainable = Cypress.Chainable; @@ -29,6 +29,16 @@ const sendEvent = (roomId: string): Chainable => { }); }; +/** generate a message event which will take up some room on the page. */ +function mkPadding(n: number): IContent { + return { + msgtype: "m.text" as MsgType, + body: `padding ${n}`, + format: "org.matrix.custom.html", + formatted_body: `

Test event ${n}

\n`.repeat(10), + }; +} + describe("Editing", () => { let homeserver: HomeserverInstance; @@ -37,7 +47,6 @@ describe("Editing", () => { homeserver = data; cy.initTestUser(homeserver, "Edith").then(() => { cy.injectAxe(); - return cy.createRoom({ name: "Test room" }).as("roomId"); }); }); }); @@ -47,6 +56,8 @@ describe("Editing", () => { }); it("should close the composer when clicking save after making a change and undoing it", () => { + cy.createRoom({ name: "Test room" }).as("roomId"); + cy.get("@roomId").then((roomId) => { sendEvent(roomId); cy.visit("/#/room/" + roomId); @@ -64,4 +75,77 @@ describe("Editing", () => { // Assert that the edit composer has gone away cy.get(".mx_EditMessageComposer").should("not.exist"); }); + + it("should correctly display events which are edited, where we lack the edit event", () => { + // This tests the behaviour when a message has been edited some time after it has been sent, and we + // jump back in room history to view the event, but do not have the actual edit event. + // + // In that scenario, we rely on the server to replace the content (pre-MSC3925), or do it ourselves based on + // the bundled edit event (post-MSC3925). + // + // To test it, we need to have a room with lots of events in, so we can jump around the timeline without + // paginating in the event itself. Hence, we create a bot user which creates the room and populates it before + // we join. + + let testRoomId: string; + let originalEventId: string; + let editEventId: string; + + // create a second user + const bobChainable = cy.getBot(homeserver, { displayName: "Bob", userIdPrefix: "bob_" }); + + cy.all([cy.window({ log: false }), bobChainable]).then(async ([win, bob]) => { + // "bob" now creates the room, and sends a load of events in it. Note that all of this happens via calls on + // the js-sdk rather than Cypress commands, so uses regular async/await. + + const room = await bob.createRoom({ name: "TestRoom", visibility: win.matrixcs.Visibility.Public }); + testRoomId = room.room_id; + cy.log(`Bot user created room ${room.room_id}`); + + originalEventId = (await bob.sendMessage(room.room_id, { body: "original", msgtype: "m.text" })).event_id; + cy.log(`Bot user sent original event ${originalEventId}`); + + // send a load of padding events. We make them large, so that they fill the whole screen + // and the client doesn't end up paginating into the event we want. + let i = 0; + while (i < 20) { + await bob.sendMessage(room.room_id, mkPadding(i++)); + } + + // ... then the edit ... + editEventId = ( + await bob.sendMessage(room.room_id, { + "m.new_content": { body: "Edited body", msgtype: "m.text" }, + "m.relates_to": { + rel_type: "m.replace", + event_id: originalEventId, + }, + "body": "* edited", + "msgtype": "m.text", + }) + ).event_id; + cy.log(`Bot user sent edit event ${editEventId}`); + + // ... then a load more padding ... + while (i < 40) { + await bob.sendMessage(room.room_id, mkPadding(i++)); + } + }); + + cy.getClient().then((cli) => { + // now have the cypress user join the room, jump to the original event, and wait for the event to be + // visible + cy.joinRoom(testRoomId); + cy.viewRoomByName("TestRoom"); + cy.visit(`#/room/${testRoomId}/${originalEventId}`); + cy.get(`[data-event-id="${originalEventId}"]`).should((messageTile) => { + // at this point, the edit event should still be unknown + expect(cli.getRoom(testRoomId).getTimelineForEvent(editEventId)).to.be.null; + + // nevertheless, the event should be updated + expect(messageTile.find(".mx_EventTile_body").text()).to.eq("Edited body"); + expect(messageTile.find(".mx_EventTile_edited")).to.exist; + }); + }); + }); });