element-web/cypress/e2e/editing/editing.spec.ts

373 lines
16 KiB
TypeScript

/*
Copyright 2022 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 { EventType, MsgType, ISendEventResponse, IContent } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import Chainable = Cypress.Chainable;
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
return cy.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.text" as MsgType,
body: "Message",
});
};
/** 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: `<h3>Test event ${n}</h3>\n`.repeat(10),
};
}
describe("Editing", () => {
let homeserver: HomeserverInstance;
let roomId: string;
// Edit "Message"
const editLastMessage = (edit: string) => {
cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Edit message" }).type(`{selectAll}{del}${edit}{enter}`);
};
const clickEditedMessage = (edited: string) => {
// Assert that the message was edited
cy.contains(".mx_EventTile", edited)
.should("exist")
.within(() => {
// Click to display the message edit history dialog
cy.contains(".mx_EventTile_edited", "(edited)").click();
});
};
const clickButtonViewSource = () => {
// Assert that "View Source" button is rendered and click it
cy.get(".mx_EventTile .mx_EventTile_line").realHover().findByRole("button", { name: "View Source" }).click();
};
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Edith").then(() => {
cy.createRoom({ name: "Test room" }).then((_room1Id) => {
roomId = _room1Id;
}),
cy.injectAxe();
});
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it("should render and interact with the message edit history dialog", () => {
// Click the "Remove" button on the message edit history dialog
const clickButtonRemove = () => {
cy.get(".mx_EventTile_line").realHover().findByRole("button", { name: "Remove" }).click();
};
cy.visit("/#/room/" + roomId);
// Send "Message"
sendEvent(roomId);
cy.get(".mx_RoomView_MessageList").within(() => {
// Edit "Message" to "Massage"
editLastMessage("Massage");
// Assert that the edit label is visible
cy.get(".mx_EventTile_edited").should("be.visible");
clickEditedMessage("Massage");
});
cy.get(".mx_Dialog").within(() => {
// Assert that the message edit history dialog is rendered
cy.get(".mx_MessageEditHistoryDialog").within(() => {
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
cy.get("li").should("have.css", "clear", "both");
cy.get(".mx_EventTile .mx_MessageTimestamp")
.should("have.css", "position", "absolute")
.should("have.css", "inset-inline-start", "0px")
.should("have.css", "text-align", "center");
// Assert that monospace characters can fill the content line as expected
cy.get(".mx_EventTile .mx_EventTile_content").should("have.css", "margin-inline-end", "0px");
// Assert that zero block start padding is applied to mx_EventTile as expected
// See: .mx_EventTile on _EventTile.pcss
cy.get(".mx_EventTile").should("have.css", "padding-block-start", "0px");
// Assert that the date separator is rendered at the top
cy.get("li:nth-child(1) .mx_TimelineSeparator").within(() => {
cy.get("h2").within(() => {
cy.findByText("today").should("have.css", "text-transform", "capitalize");
});
});
// Assert that the edited message is rendered under the date separator
cy.get("li:nth-child(2) .mx_EventTile").within(() => {
// Assert that the edited message body consists of both deleted character and inserted character
// Above the first "e" of "Message" was replaced with "a"
cy.get(".mx_EventTile_content .mx_EventTile_body").should("have.text", "Meassage");
cy.get(".mx_EventTile_content .mx_EventTile_body").within(() => {
cy.get(".mx_EditHistoryMessage_deletion").within(() => {
cy.findByText("e");
});
cy.get(".mx_EditHistoryMessage_insertion").within(() => {
cy.findByText("a");
});
});
});
// Assert that the original message is rendered at the bottom
cy.get("li:nth-child(3) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").within(() => {
cy.findByText("Message");
});
});
});
});
// Exclude timestamps from a snapshot
const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
// Take a snapshot of the dialog
cy.get(".mx_Dialog_wrapper").percySnapshotElement("Message edit history dialog", { percyCSS });
cy.get(".mx_Dialog").within(() => {
cy.get(".mx_MessageEditHistoryDialog li:nth-child(2) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").should("have.text", "Meassage");
// Click the "Remove" button again
clickButtonRemove();
});
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
cy.get(".mx_TextInputDialog").closeDialog();
// Assert that the message edit history dialog is rendered again after it was closed
cy.get(".mx_MessageEditHistoryDialog li:nth-child(2) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").should("have.text", "Meassage");
// Click the "Remove" button again
clickButtonRemove();
});
// This time remove the message really
cy.get(".mx_TextInputDialog").within(() => {
cy.findByRole("textbox", { name: "Reason (optional)" }).type("This is a test."); // Reason
cy.findByRole("button", { name: "Remove" }).click();
});
// Assert that the message edit history dialog is rendered again
cy.get(".mx_MessageEditHistoryDialog").within(() => {
// Assert that the date is rendered
cy.get("li:nth-child(1) .mx_TimelineSeparator").within(() => {
cy.get("h2").within(() => {
cy.findByText("today").should("have.css", "text-transform", "capitalize");
});
});
// Assert that the original message is rendered under the date on the dialog
cy.get("li:nth-child(2) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").within(() => {
cy.findByText("Message");
});
});
// Assert that the edited message is gone
cy.contains(".mx_EventTile_content .mx_EventTile_body", "Meassage").should("not.exist");
cy.closeDialog();
});
});
// Assert that the main timeline is rendered
cy.get(".mx_RoomView_MessageList").within(() => {
cy.get(".mx_EventTile_last .mx_RedactedBody").within(() => {
// Assert that the placeholder is rendered
cy.findByText("Message deleted");
});
});
});
it("should render 'View Source' button in developer mode on the message edit history dialog", () => {
cy.visit("/#/room/" + roomId);
// Send "Message"
sendEvent(roomId);
cy.get(".mx_RoomView_MessageList").within(() => {
// Edit "Message" to "Massage"
editLastMessage("Massage");
// Assert that the edit label is visible
cy.get(".mx_EventTile_edited").should("be.visible");
clickEditedMessage("Massage");
});
cy.get(".mx_Dialog").within(() => {
// Assert that the original message is rendered
cy.get(".mx_MessageEditHistoryDialog li:nth-child(3)").within(() => {
// Assert that "View Source" is not rendered
cy.get(".mx_EventTile .mx_EventTile_line")
.realHover()
.findByRole("button", { name: "View Source" })
.should("not.exist");
});
cy.closeDialog();
});
// Enable developer mode
cy.setSettingValue("developerMode", null, SettingLevel.ACCOUNT, true);
cy.get(".mx_RoomView_MessageList").within(() => {
clickEditedMessage("Massage");
});
cy.get(".mx_Dialog").within(() => {
// Assert that the edited message is rendered
cy.get(".mx_MessageEditHistoryDialog li:nth-child(2)").within(() => {
// Assert that "Remove" button for the original message is rendered
cy.get(".mx_EventTile .mx_EventTile_line").realHover().findByRole("button", { name: "Remove" });
clickButtonViewSource();
});
// Assert that view source dialog is rendered and close the dialog
cy.get(".mx_ViewSource").closeDialog();
// Assert that the original message is rendered
cy.get(".mx_MessageEditHistoryDialog li:nth-child(3)").within(() => {
// Assert that "Remove" button for the original message does not exist
cy.get(".mx_EventTile .mx_EventTile_line")
.realHover()
.findByRole("button", { name: "Remove" })
.should("not.exist");
clickButtonViewSource();
});
// Assert that view source dialog is rendered and close the dialog
cy.get(".mx_ViewSource").closeDialog();
});
});
it("should close the composer when clicking save after making a change and undoing it", () => {
cy.visit("/#/room/" + roomId);
sendEvent(roomId);
// Edit message
cy.get(".mx_RoomView_body .mx_EventTile").within(() => {
cy.findByText("Message");
cy.get(".mx_EventTile_line").realHover().findByRole("button", { name: "Edit" }).click().checkA11y();
cy.get(".mx_EventTile_line")
.findByRole("textbox", { name: "Edit message" })
.type("Foo{backspace}{backspace}{backspace}{enter}")
.checkA11y();
});
cy.get(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]").within(() => {
cy.findByText("Message");
});
// Assert that the edit composer has gone away
cy.findByRole("textbox", { name: "Edit message" }).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 < 10) {
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 < 20) {
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;
});
});
});
});