diff --git a/cypress/e2e/14-timeline/timeline.spec.ts b/cypress/e2e/14-timeline/timeline.spec.ts
new file mode 100644
index 0000000000..22861c8fd7
--- /dev/null
+++ b/cypress/e2e/14-timeline/timeline.spec.ts
@@ -0,0 +1,145 @@
+/*
+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.
+*/
+
+///
+
+import { MessageEvent } from "matrix-events-sdk";
+
+import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
+import type { EventType } from "matrix-js-sdk/src/@types/event";
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+import Chainable = Cypress.Chainable;
+
+// The avatar size used in the timeline
+const AVATAR_SIZE = 30;
+// The resize method used in the timeline
+const AVATAR_RESIZE_METHOD = "crop";
+
+const ROOM_NAME = "Test room";
+const OLD_AVATAR = "avatar_image1";
+const NEW_AVATAR = "avatar_image2";
+const OLD_NAME = "Alan";
+const NEW_NAME = "Alan (away)";
+
+const getEventTilesWithBodies = (): Chainable => {
+ return cy.get(".mx_EventTile").filter((_i, e) => e.getElementsByClassName("mx_EventTile_body").length > 0);
+};
+
+const expectDisplayName = (e: JQuery, displayName: string): void => {
+ expect(e.find(".mx_DisambiguatedProfile_displayName").text()).to.equal(displayName);
+};
+
+const expectAvatar = (e: JQuery, avatarUrl: string): void => {
+ cy.getClient().then((cli: MatrixClient) => {
+ expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
+ // eslint-disable-next-line no-restricted-properties
+ cli.mxcUrlToHttp(avatarUrl, AVATAR_SIZE, AVATAR_SIZE, AVATAR_RESIZE_METHOD),
+ );
+ });
+};
+
+const sendEvent = (roomId: string): Chainable => {
+ return cy.sendEvent(
+ roomId,
+ null,
+ "m.room.message" as EventType,
+ MessageEvent.from("Message").serialize().content,
+ );
+};
+
+describe("Timeline", () => {
+ let synapse: SynapseInstance;
+
+ let roomId: string;
+
+ let oldAvatarUrl: string;
+ let newAvatarUrl: string;
+
+ describe("useOnlyCurrentProfiles", () => {
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+ cy.initTestUser(synapse, OLD_NAME).then(() =>
+ cy.window({ log: false }).then(() => {
+ cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
+ roomId = _room1Id;
+ });
+ }),
+ ).then(() => {
+ cy.uploadContent(OLD_AVATAR).then((url) => {
+ oldAvatarUrl = url;
+ cy.setAvatarUrl(url);
+ });
+ }).then(() => {
+ cy.uploadContent(NEW_AVATAR).then((url) => {
+ newAvatarUrl = url;
+ });
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should show historical profiles if disabled", () => {
+ cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, false);
+ sendEvent(roomId);
+ cy.setDisplayName("Alan (away)");
+ cy.setAvatarUrl(newAvatarUrl);
+ // XXX: If we send the second event too quickly, there won't be
+ // enough time for the client to register the profile change
+ cy.wait(500);
+ sendEvent(roomId);
+ cy.viewRoomByName(ROOM_NAME);
+
+ const events = getEventTilesWithBodies();
+
+ events.should("have.length", 2);
+ events.each((e, i) => {
+ if (i === 0) {
+ expectDisplayName(e, OLD_NAME);
+ expectAvatar(e, oldAvatarUrl);
+ } else if (i === 1) {
+ expectDisplayName(e, NEW_NAME);
+ expectAvatar(e, newAvatarUrl);
+ }
+ });
+ });
+
+ it("should not show historical profiles if enabled", () => {
+ cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, true);
+ sendEvent(roomId);
+ cy.setDisplayName(NEW_NAME);
+ cy.setAvatarUrl(newAvatarUrl);
+ // XXX: If we send the second event too quickly, there won't be
+ // enough time for the client to register the profile change
+ cy.wait(500);
+ sendEvent(roomId);
+ cy.viewRoomByName(ROOM_NAME);
+
+ const events = getEventTilesWithBodies();
+
+ events.should("have.length", 2);
+ events.each((e) => {
+ expectDisplayName(e, NEW_NAME);
+ expectAvatar(e, newAvatarUrl);
+ });
+ });
+ });
+});