diff --git a/cypress/e2e/sliding-sync/sliding-sync.spec.ts b/cypress/e2e/sliding-sync/sliding-sync.spec.ts
deleted file mode 100644
index ee9beafe14..0000000000
--- a/cypress/e2e/sliding-sync/sliding-sync.spec.ts
+++ /dev/null
@@ -1,502 +0,0 @@
-/*
-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 _ from "lodash";
-import { MatrixClient } from "matrix-js-sdk/src/matrix";
-import { Interception } from "cypress/types/net-stubbing";
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { ProxyInstance } from "../../plugins/sliding-sync";
-
-describe("Sliding Sync", () => {
- beforeEach(() => {
- cy.startHomeserver("default")
- .as("homeserver")
- .then((homeserver) => {
- cy.startProxy(homeserver).as("proxy");
- });
-
- cy.all([cy.get("@homeserver"), cy.get("@proxy")]).then(
- ([homeserver, proxy]) => {
- cy.enableLabsFeature("feature_sliding_sync");
-
- cy.intercept("/config.json?cachebuster=*", (req) => {
- return req.continue((res) => {
- res.send(200, {
- ...res.body,
- setting_defaults: {
- feature_sliding_sync_proxy_url: `http://localhost:${proxy.port}`,
- },
- });
- });
- });
-
- cy.initTestUser(homeserver, "Sloth").then(() => {
- return cy.window({ log: false }).then(() => {
- cy.createRoom({ name: "Test Room" }).as("roomId");
- });
- });
- },
- );
- });
-
- afterEach(() => {
- cy.get("@homeserver").then(cy.stopHomeserver);
- cy.get("@proxy").then(cy.stopProxy);
- });
-
- // assert order
- const checkOrder = (wantOrder: string[]) => {
- cy.findByRole("group", { name: "Rooms" })
- .find(".mx_RoomTile_title")
- .should((elements) => {
- expect(
- _.map(elements, (e) => {
- return e.textContent;
- }),
- "rooms are sorted",
- ).to.deep.equal(wantOrder);
- });
- };
- const bumpRoom = (alias: string) => {
- // Send a message into the given room, this should bump the room to the top
- cy.get(alias).then((roomId) => {
- return cy.sendEvent(roomId, null, "m.room.message", {
- body: "Hello world",
- msgtype: "m.text",
- });
- });
- };
- const createAndJoinBob = () => {
- // create a Bob user
- cy.get("@homeserver").then((homeserver) => {
- return cy
- .getBot(homeserver, {
- displayName: "Bob",
- })
- .as("bob");
- });
-
- // invite Bob to Test Room and accept then send a message.
- cy.all([cy.get("@roomId"), cy.get("@bob")]).then(([roomId, bob]) => {
- return cy.inviteUser(roomId, bob.getUserId()).then(() => {
- return bob.joinRoom(roomId);
- });
- });
- };
-
- it.skip("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => {
- // create rooms and check room names are correct
- cy.createRoom({ name: "Apple" }).then(() => cy.findByRole("treeitem", { name: "Apple" }));
- cy.createRoom({ name: "Pineapple" }).then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
- cy.createRoom({ name: "Orange" }).then(() => cy.findByRole("treeitem", { name: "Orange" }));
-
- cy.get(".mx_RoomSublist_tiles").within(() => {
- cy.findAllByRole("treeitem").should("have.length", 4); // due to the Test Room in beforeEach
- });
-
- checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
-
- cy.findByRole("group", { name: "Rooms" }).within(() => {
- cy.get(".mx_RoomSublist_headerContainer")
- .realHover()
- .findByRole("button", { name: "List options" })
- .click();
- });
-
- // force click as the radio button's size is zero
- cy.findByRole("menuitemradio", { name: "A-Z" }).click({ force: true });
-
- // Assert that the radio button is checked
- cy.get(".mx_StyledRadioButton_checked").within(() => {
- cy.findByText("A-Z").should("exist");
- });
-
- checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
- });
-
- it.skip("should move rooms around as new events arrive", () => {
- // create rooms and check room names are correct
- cy.createRoom({ name: "Apple" })
- .as("roomA")
- .then(() => cy.findByRole("treeitem", { name: "Apple" }));
- cy.createRoom({ name: "Pineapple" })
- .as("roomP")
- .then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
- cy.createRoom({ name: "Orange" })
- .as("roomO")
- .then(() => cy.findByRole("treeitem", { name: "Orange" }));
-
- // Select the Test Room
- cy.findByRole("treeitem", { name: "Test Room" }).click();
-
- checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
- bumpRoom("@roomA");
- checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
- bumpRoom("@roomO");
- checkOrder(["Orange", "Apple", "Pineapple", "Test Room"]);
- bumpRoom("@roomO");
- checkOrder(["Orange", "Apple", "Pineapple", "Test Room"]);
- bumpRoom("@roomP");
- checkOrder(["Pineapple", "Orange", "Apple", "Test Room"]);
- });
-
- it.skip("should not move the selected room: it should be sticky", () => {
- // create rooms and check room names are correct
- cy.createRoom({ name: "Apple" })
- .as("roomA")
- .then(() => cy.findByRole("treeitem", { name: "Apple" }));
- cy.createRoom({ name: "Pineapple" })
- .as("roomP")
- .then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
- cy.createRoom({ name: "Orange" })
- .as("roomO")
- .then(() => cy.findByRole("treeitem", { name: "Orange" }));
-
- // Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
- // turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically
- // be Apple, Orange Pineapple - only when you click on a different room do things reshuffle.
-
- // Select the Pineapple room
- cy.findByRole("treeitem", { name: "Pineapple" }).click();
- checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
-
- // Move Apple
- bumpRoom("@roomA");
- checkOrder(["Apple", "Pineapple", "Orange", "Test Room"]);
-
- // Select the Test Room
- cy.findByRole("treeitem", { name: "Test Room" }).click();
-
- // the rooms reshuffle to match reality
- checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
- });
-
- it.skip("should show the right unread notifications", () => {
- createAndJoinBob();
-
- // send a message in the test room: unread notif count shoould increment
- cy.all([cy.get("@roomId"), cy.get("@bob")]).then(([roomId, bob]) => {
- return bob.sendTextMessage(roomId, "Hello World");
- });
-
- // check that there is an unread notification (grey) as 1
- cy.findByRole("treeitem", { name: "Test Room 1 unread message." }).contains(".mx_NotificationBadge_count", "1");
- cy.get(".mx_NotificationBadge").should("not.have.class", "mx_NotificationBadge_highlighted");
-
- // send an @mention: highlight count (red) should be 2.
- cy.all([cy.get("@roomId"), cy.get("@bob")]).then(([roomId, bob]) => {
- return bob.sendTextMessage(roomId, "Hello Sloth");
- });
- cy.findByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).contains(
- ".mx_NotificationBadge_count",
- "2",
- );
- cy.get(".mx_NotificationBadge").should("have.class", "mx_NotificationBadge_highlighted");
-
- // click on the room, the notif counts should disappear
- cy.findByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).click();
- cy.findByRole("treeitem", { name: "Test Room" }).should("not.have.class", "mx_NotificationBadge_count");
- });
-
- it.skip("should not show unread indicators", () => {
- // TODO: for now. Later we should.
- createAndJoinBob();
-
- // disable notifs in this room (TODO: CS API call?)
- cy.findByRole("treeitem", { name: "Test Room" })
- .realHover()
- .findByRole("button", { name: "Notification options" })
- .click();
- cy.findByRole("menuitemradio", { name: "Mute room" }).click();
-
- // create a new room so we know when the message has been received as it'll re-shuffle the room list
- cy.createRoom({
- name: "Dummy",
- });
- checkOrder(["Dummy", "Test Room"]);
-
- cy.all([cy.get("@roomId"), cy.get("@bob")]).then(([roomId, bob]) => {
- return bob.sendTextMessage(roomId, "Do you read me?");
- });
- // wait for this message to arrive, tell by the room list resorting
- checkOrder(["Test Room", "Dummy"]);
-
- cy.findByRole("treeitem", { name: "Test Room" }).get(".mx_NotificationBadge").should("not.exist");
- });
-
- it("should update user settings promptly", () => {
- cy.openUserSettings("Preferences");
- cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format")
- .should("exist")
- .find(".mx_ToggleSwitch_on")
- .should("not.exist");
- cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format")
- .should("exist")
- .find(".mx_ToggleSwitch_ball")
- .click();
- cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 })
- .should("exist")
- .find(".mx_ToggleSwitch_on", { timeout: 2000 })
- .should("exist");
- });
-
- it.skip("should show and be able to accept/reject/rescind invites", () => {
- createAndJoinBob();
-
- let clientUserId;
- cy.getClient().then((cli) => {
- clientUserId = cli.getUserId();
- });
-
- // invite Sloth into 3 rooms:
- // - roomJoin: will join this room
- // - roomReject: will reject the invite
- // - roomRescind: will make Bob rescind the invite
- let roomJoin;
- let roomReject;
- let roomRescind;
- let bobClient;
- cy.get("@bob")
- .then((bob) => {
- bobClient = bob;
- return Promise.all([
- bob.createRoom({ name: "Room to Join" }),
- bob.createRoom({ name: "Room to Reject" }),
- bob.createRoom({ name: "Room to Rescind" }),
- ]);
- })
- .then(([join, reject, rescind]) => {
- roomJoin = join.room_id;
- roomReject = reject.room_id;
- roomRescind = rescind.room_id;
- return Promise.all([
- bobClient.invite(roomJoin, clientUserId),
- bobClient.invite(roomReject, clientUserId),
- bobClient.invite(roomRescind, clientUserId),
- ]);
- });
-
- cy.findByRole("group", { name: "Invites" }).within(() => {
- // Exclude headerText
- cy.get(".mx_RoomSublist_tiles").within(() => {
- // Wait for them all to be on the UI
- cy.findAllByRole("treeitem").should("have.length", 3);
- });
- });
-
- // Select the room to join
- cy.findByRole("treeitem", { name: "Room to Join" }).click();
-
- cy.get(".mx_RoomView").within(() => {
- // Accept the invite
- cy.findByRole("button", { name: "Accept" }).click();
- });
-
- checkOrder(["Room to Join", "Test Room"]);
-
- // Select the room to reject
- cy.findByRole("treeitem", { name: "Room to Reject" }).click();
-
- cy.get(".mx_RoomView").within(() => {
- // Reject the invite
- cy.findByRole("button", { name: "Reject" }).click();
- });
-
- cy.findByRole("group", { name: "Invites" }).within(() => {
- // Exclude headerText
- cy.get(".mx_RoomSublist_tiles").within(() => {
- // Wait for the rejected room to disappear
- cy.findAllByRole("treeitem").should("have.length", 2);
- });
- });
-
- // check the lists are correct
- checkOrder(["Room to Join", "Test Room"]);
-
- cy.findByRole("group", { name: "Invites" })
- .find(".mx_RoomTile_title")
- .should((elements) => {
- expect(
- _.map(elements, (e) => {
- return e.textContent;
- }),
- "rooms are sorted",
- ).to.deep.equal(["Room to Rescind"]);
- });
-
- // now rescind the invite
- cy.get("@bob").then((bob) => {
- return bob.kick(roomRescind, clientUserId);
- });
-
- cy.findByRole("group", { name: "Rooms" }).within(() => {
- // Exclude headerText
- cy.get(".mx_RoomSublist_tiles").within(() => {
- // Wait for the rescind to take effect and check the joined list once more
- cy.findAllByRole("treeitem").should("have.length", 2);
- });
- });
-
- checkOrder(["Room to Join", "Test Room"]);
- });
-
- it("should show a favourite DM only in the favourite sublist", () => {
- cy.createRoom({
- name: "Favourite DM",
- is_direct: true,
- })
- .as("room")
- .then((roomId) => {
- cy.getClient().then((cli) => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
- });
-
- cy.findByRole("group", { name: "Favourites" }).findByText("Favourite DM").should("exist");
- cy.findByRole("group", { name: "People" }).findByText("Favourite DM").should("not.exist");
- });
-
- // Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.
- // This ensures we are setting RoomViewStore state correctly.
- it.skip("should clear the reply to field when swapping rooms", () => {
- cy.createRoom({ name: "Other Room" })
- .as("roomA")
- .then(() => cy.findByRole("treeitem", { name: "Other Room" }));
- cy.get("@roomId").then((roomId) => {
- return cy.sendEvent(roomId, null, "m.room.message", {
- body: "Hello world",
- msgtype: "m.text",
- });
- });
- // select the room
- cy.findByRole("treeitem", { name: "Test Room" }).click();
- cy.get(".mx_ReplyPreview").should("not.exist");
- // click reply-to on the Hello World message
- cy.get(".mx_EventTile_last")
- .within(() => {
- cy.findByText("Hello world", { timeout: 1000 });
- })
- .realHover()
- .findByRole("button", { name: "Reply" })
- .click();
- // check it's visible
- cy.get(".mx_ReplyPreview").should("exist");
- // now click Other Room
- cy.findByRole("treeitem", { name: "Other Room" }).click();
- // ensure the reply-to disappears
- cy.get(".mx_ReplyPreview").should("not.exist");
- // click back
- cy.findByRole("treeitem", { name: "Test Room" }).click();
- // ensure the reply-to reappears
- cy.get(".mx_ReplyPreview").should("exist");
- });
-
- // Regression test for https://github.com/vector-im/element-web/issues/21462
- it.skip("should not cancel replies when permalinks are clicked", () => {
- cy.get("@roomId").then((roomId) => {
- // we require a first message as you cannot click the permalink text with the avatar in the way
- return cy
- .sendEvent(roomId, null, "m.room.message", {
- body: "First message",
- msgtype: "m.text",
- })
- .then(() => {
- return cy.sendEvent(roomId, null, "m.room.message", {
- body: "Permalink me",
- msgtype: "m.text",
- });
- })
- .then(() => {
- cy.sendEvent(roomId, null, "m.room.message", {
- body: "Reply to me",
- msgtype: "m.text",
- });
- });
- });
- // select the room
- cy.findByRole("treeitem", { name: "Test Room" }).click();
- cy.get(".mx_ReplyPreview").should("not.exist");
- // click reply-to on the Reply to me message
- cy.get(".mx_EventTile")
- .last()
- .within(() => {
- cy.findByText("Reply to me");
- })
- .realHover()
- .findByRole("button", { name: "Reply" })
- .click();
- // check it's visible
- cy.get(".mx_ReplyPreview").should("exist");
- // now click on the permalink for Permalink me
- cy.contains(".mx_EventTile", "Permalink me").find("a").click({ force: true });
- // make sure it is now selected with the little green |
- cy.contains(".mx_EventTile_selected", "Permalink me").should("exist");
- // ensure the reply-to does not disappear
- cy.get(".mx_ReplyPreview").should("exist");
- });
-
- it.skip("should send unsubscribe_rooms for every room switch", () => {
- let roomAId: string;
- let roomPId: string;
- // create rooms and check room names are correct
- cy.createRoom({ name: "Apple" })
- .as("roomA")
- .then((roomId) => (roomAId = roomId))
- .then(() => cy.findByRole("treeitem", { name: "Apple" }));
-
- cy.createRoom({ name: "Pineapple" })
- .as("roomP")
- .then((roomId) => (roomPId = roomId))
- .then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
- cy.createRoom({ name: "Orange" })
- .as("roomO")
- .then(() => cy.findByRole("treeitem", { name: "Orange" }));
-
- // Intercept all calls to /sync
- cy.intercept({ method: "POST", url: "**/sync*" }).as("syncRequest");
-
- const assertUnsubExists = (interception: Interception, subRoomId: string, unsubRoomId: string) => {
- const body = interception.request.body;
- // There may be a request without a txn_id, ignore it, as there won't be any subscription changes
- if (body.txn_id === undefined) {
- return;
- }
- expect(body.unsubscribe_rooms).eql([unsubRoomId]);
- expect(body.room_subscriptions).to.not.have.property(unsubRoomId);
- expect(body.room_subscriptions).to.have.property(subRoomId);
- };
-
- // Select the Test Room
- cy.findByRole("treeitem", { name: "Apple" }).click();
-
- // and wait for cypress to get the result as alias
- cy.wait("@syncRequest").then((interception) => {
- // This is the first switch, so no unsubscriptions yet.
- assert.isObject(interception.request.body.room_subscriptions, "room_subscriptions is object");
- });
-
- // Switch to another room
- cy.findByRole("treeitem", { name: "Pineapple" }).click();
- cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId));
-
- // And switch to even another room
- cy.findByRole("treeitem", { name: "Apple" }).click();
- cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId));
-
- // TODO: Add tests for encrypted rooms
- });
-});
diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts
index a0bd7a5c7f..d13f025caa 100644
--- a/cypress/plugins/index.ts
+++ b/cypress/plugins/index.ts
@@ -22,7 +22,6 @@ import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { synapseDocker } from "./synapsedocker";
import { dendriteDocker } from "./dendritedocker";
-import { slidingSyncProxyDocker } from "./sliding-sync";
import { webserver } from "./webserver";
import { docker } from "./docker";
import { log } from "./log";
@@ -31,7 +30,7 @@ import { log } from "./log";
* @type {Cypress.PluginConfig}
*/
export default function (on: PluginEvents, config: PluginConfigOptions) {
- initPlugins(on, [docker, synapseDocker, dendriteDocker, slidingSyncProxyDocker, webserver, log], config);
+ initPlugins(on, [docker, synapseDocker, dendriteDocker, webserver, log], config);
installLogsPrinter(on, {
printLogsToConsole: "never",
diff --git a/cypress/plugins/sliding-sync/index.ts b/cypress/plugins/sliding-sync/index.ts
deleted file mode 100644
index ab39c7a42b..0000000000
--- a/cypress/plugins/sliding-sync/index.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
-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 PluginEvents = Cypress.PluginEvents;
-import PluginConfigOptions = Cypress.PluginConfigOptions;
-import { dockerExec, dockerIp, dockerRun, dockerStop } from "../docker";
-import { getFreePort } from "../utils/port";
-import { HomeserverInstance } from "../utils/homeserver";
-
-// A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync
-// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
-
-export interface ProxyInstance {
- containerId: string;
- postgresId: string;
- port: number;
-}
-
-const instances = new Map();
-
-const PG_PASSWORD = "p4S5w0rD";
-
-async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Promise {
- console.log(new Date(), "Starting sliding sync proxy...");
-
- const postgresId = await dockerRun({
- image: "postgres",
- containerName: "react-sdk-cypress-sliding-sync-postgres",
- params: ["--rm", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`],
- });
-
- const postgresIp = await dockerIp({ containerId: postgresId });
- const homeserverIp = await dockerIp({ containerId: homeserver.serverId });
- console.log(new Date(), "postgres container up");
-
- const waitTimeMillis = 30000;
- const startTime = new Date().getTime();
- let lastErr: Error;
- while (new Date().getTime() - startTime < waitTimeMillis) {
- try {
- await dockerExec({
- containerId: postgresId,
- params: ["pg_isready", "-U", "postgres"],
- });
- lastErr = null;
- break;
- } catch (err) {
- console.log("pg_isready: failed");
- lastErr = err;
- }
- }
- if (lastErr) {
- console.log("rethrowing");
- throw lastErr;
- }
-
- const port = await getFreePort();
- console.log(new Date(), "starting proxy container...", dockerTag);
- const containerId = await dockerRun({
- image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag,
- containerName: "react-sdk-cypress-sliding-sync-proxy",
- params: [
- "--rm",
- "-p",
- `${port}:8008/tcp`,
- "-e",
- "SYNCV3_SECRET=bwahahaha",
- "-e",
- `SYNCV3_SERVER=http://${homeserverIp}:8008`,
- "-e",
- `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
- ],
- });
- console.log(new Date(), "started!");
-
- const instance: ProxyInstance = { containerId, postgresId, port };
- instances.set(containerId, instance);
- return instance;
-}
-
-async function proxyStop(instance: ProxyInstance): Promise {
- await dockerStop({
- containerId: instance.containerId,
- });
- await dockerStop({
- containerId: instance.postgresId,
- });
-
- instances.delete(instance.containerId);
-
- console.log(new Date(), "Stopped sliding sync proxy.");
- // cypress deliberately fails if you return 'undefined', so
- // return null to signal all is well, and we've handled the task.
- return null;
-}
-
-/**
- * @type {Cypress.PluginConfig}
- */
-export function slidingSyncProxyDocker(on: PluginEvents, config: PluginConfigOptions) {
- const dockerTag = config.env["SLIDING_SYNC_PROXY_TAG"];
-
- on("task", {
- proxyStart: proxyStart.bind(null, dockerTag),
- proxyStop,
- });
-
- on("after:spec", async (spec) => {
- for (const instance of instances.values()) {
- console.warn(`Cleaning up proxy on port ${instance.port} after ${spec.name}`);
- await proxyStop(instance);
- }
- });
-}
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
index 5cec756741..cce1534cd9 100644
--- a/cypress/support/e2e.ts
+++ b/cypress/support/e2e.ts
@@ -38,7 +38,6 @@ import "./iframes";
import "./timeline";
import "./network";
import "./composer";
-import "./proxy";
import "./axe";
import "./promise";
diff --git a/cypress/support/proxy.ts b/cypress/support/proxy.ts
deleted file mode 100644
index b40584ec7f..0000000000
--- a/cypress/support/proxy.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-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 Chainable = Cypress.Chainable;
-import AUTWindow = Cypress.AUTWindow;
-import { ProxyInstance } from "../plugins/sliding-sync";
-import { HomeserverInstance } from "../plugins/utils/homeserver";
-
-declare global {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace Cypress {
- interface Chainable {
- /**
- * Start a sliding sync proxy instance.
- * @param homeserver the homeserver instance returned by startHomeserver
- */
- startProxy(homeserver: HomeserverInstance): Chainable;
-
- /**
- * Custom command wrapping task:proxyStop whilst preventing uncaught exceptions
- * for if Docker stopping races with the app's background sync loop.
- * @param proxy the proxy instance returned by startProxy
- */
- stopProxy(proxy: ProxyInstance): Chainable;
- }
- }
-}
-
-function startProxy(homeserver: HomeserverInstance): Chainable {
- return cy.task("proxyStart", homeserver);
-}
-
-function stopProxy(proxy?: ProxyInstance): Chainable {
- if (!proxy) return;
- // Navigate away from app to stop the background network requests which will race with Homeserver shutting down
- return cy.window({ log: false }).then((win) => {
- win.location.href = "about:blank";
- cy.task("proxyStop", proxy);
- });
-}
-
-Cypress.Commands.add("startProxy", startProxy);
-Cypress.Commands.add("stopProxy", stopProxy);
diff --git a/playwright/e2e/right-panel/notification-panel.spec.ts b/playwright/e2e/right-panel/notification-panel.spec.ts
index 9e3f7e03de..6223c1c13f 100644
--- a/playwright/e2e/right-panel/notification-panel.spec.ts
+++ b/playwright/e2e/right-panel/notification-panel.spec.ts
@@ -22,6 +22,7 @@ const NAME = "Alice";
test.describe("NotificationPanel", () => {
test.use({
displayName: NAME,
+ labsFlags: ["feature_notifications"],
});
test.beforeEach(async ({ app, user }) => {
@@ -29,7 +30,6 @@ test.describe("NotificationPanel", () => {
});
test("should render empty state", async ({ page, app }) => {
- await app.labs.enableLabsFeature("feature_notifications");
await app.viewRoomByName(ROOM_NAME);
await page.getByRole("button", { name: "Notifications" }).click();
diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts
index 45bb6a6810..ab9ed9ff7e 100644
--- a/playwright/e2e/room/room-header.spec.ts
+++ b/playwright/e2e/room/room-header.spec.ts
@@ -25,10 +25,9 @@ test.describe("Room Header", () => {
});
test.describe("with feature_notifications enabled", () => {
- test.beforeEach(async ({ app }) => {
- await app.labs.enableLabsFeature("feature_notifications");
+ test.use({
+ labsFlags: ["feature_notifications"],
});
-
test("should render default buttons properly", async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
@@ -101,9 +100,7 @@ test.describe("Room Header", () => {
});
test.describe("with feature_pinning enabled", () => {
- test.beforeEach(async ({ app }) => {
- await app.labs.enableLabsFeature("feature_pinning");
- });
+ test.use({ labsFlags: ["feature_pinning"] });
test("should render the pin button for pinned messages card", async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room" });
@@ -126,9 +123,7 @@ test.describe("Room Header", () => {
});
test.describe("with a video room", () => {
- test.beforeEach(async ({ app }) => {
- await app.labs.enableLabsFeature("feature_video_rooms");
- });
+ test.use({ labsFlags: ["feature_video_rooms"] });
const createVideoRoom = async (page: Page, app: ElementAppPage) => {
await page.locator(".mx_LeftPanel_roomListContainer").getByRole("button", { name: "Add room" }).click();
@@ -142,33 +137,36 @@ test.describe("Room Header", () => {
await app.viewRoomByName("Test video room");
};
- test("should render buttons for room options, beta pill, invite, chat, and room info", async ({
- page,
- app,
- user,
- }) => {
- await app.labs.enableLabsFeature("feature_notifications");
- await createVideoRoom(page, app);
+ test.describe("and with feature_notifications enabled", () => {
+ test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
- const header = page.locator(".mx_LegacyRoomHeader");
- // Names (aria-label) of the buttons on the video room header
- const expectedButtonNames = [
- "Room options",
- "Video rooms are a beta feature Click for more info", // Beta pill
- "Invite",
- "Chat",
- "Room info",
- ];
+ test("should render buttons for room options, beta pill, invite, chat, and room info", async ({
+ page,
+ app,
+ user,
+ }) => {
+ await createVideoRoom(page, app);
- // Assert they are found and visible
- for (const name of expectedButtonNames) {
- await expect(header.getByRole("button", { name })).toBeVisible();
- }
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Names (aria-label) of the buttons on the video room header
+ const expectedButtonNames = [
+ "Room options",
+ "Video rooms are a beta feature Click for more info", // Beta pill
+ "Invite",
+ "Chat",
+ "Room info",
+ ];
- // Assert that there is not a button except those buttons
- await expect(header.getByRole("button")).toHaveCount(7);
+ // Assert they are found and visible
+ for (const name of expectedButtonNames) {
+ await expect(header.getByRole("button", { name })).toBeVisible();
+ }
- await expect(header).toMatchScreenshot("room-header-video-room.png");
+ // Assert that there is not a button except those buttons
+ await expect(header.getByRole("button")).toHaveCount(7);
+
+ await expect(header).toMatchScreenshot("room-header-video-room.png");
+ });
});
test("should render a working chat button which opens the timeline on a right panel", async ({
diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts
new file mode 100644
index 0000000000..1a9fa08d29
--- /dev/null
+++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts
@@ -0,0 +1,375 @@
+/*
+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 { Page, Request } from "@playwright/test";
+
+import { test, expect } from "../../element-web-test";
+import type { ElementAppPage } from "../../pages/ElementAppPage";
+import type { Bot } from "../../pages/bot";
+
+test.describe("Sliding Sync", () => {
+ let roomId: string;
+
+ test.beforeEach(async ({ slidingSyncProxy, page, user, app }) => {
+ roomId = await app.client.createRoom({ name: "Test Room" });
+ });
+
+ const checkOrder = async (wantOrder: string[], page: Page) => {
+ await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
+ };
+
+ const bumpRoom = async (roomId: string, app: ElementAppPage) => {
+ // Send a message into the given room, this should bump the room to the top
+ console.log("sendEvent", app.client.sendEvent);
+ await app.client.sendEvent(roomId, null, "m.room.message", {
+ body: "Hello world",
+ msgtype: "m.text",
+ });
+ };
+
+ const createAndJoinBot = async (app: ElementAppPage, bot: Bot): Promise => {
+ await bot.prepareClient();
+ const bobUserId = await bot.evaluate((client) => client.getUserId());
+ await app.client.evaluate(
+ async (client, { bobUserId, roomId }) => {
+ await client.invite(roomId, bobUserId);
+ },
+ { bobUserId, roomId },
+ );
+ await bot.joinRoom(roomId);
+ return bot;
+ };
+
+ test.skip("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", async ({
+ page,
+ app,
+ }) => {
+ // create rooms and check room names are correct
+ for (const fruit of ["Apple", "Pineapple", "Orange"]) {
+ await app.client.createRoom({ name: fruit });
+ await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ }
+
+ // Check count, 3 fruits + 1 room created in beforeEach = 4
+ await expect(page.locator(".mx_RoomSublist_tiles").getByRole("treeitem")).toHaveCount(4);
+ await checkOrder(["Orange", "Pineapple", "Apple", "Test Room"], page);
+
+ const locator = page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomSublist_headerContainer");
+ await locator.hover();
+ await locator.getByRole("button", { name: "List options" }).click();
+
+ // force click as the radio button's size is zero
+ await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click");
+ await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible();
+
+ await page.pause();
+ await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
+ });
+
+ test.skip("should move rooms around as new events arrive", async ({ page, app }) => {
+ // create rooms and check room names are correct
+ const roomIds: string[] = [];
+ for (const fruit of ["Apple", "Pineapple", "Orange"]) {
+ const id = await app.client.createRoom({ name: fruit });
+ roomIds.push(id);
+ await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ }
+
+ // Select the Test Room
+ await page.getByRole("treeitem", { name: "Test Room" }).click();
+ const [apple, pineapple, orange] = roomIds;
+ await checkOrder(["Orange", "Pineapple", "Apple", "Test Room"], page);
+ await bumpRoom(apple, app);
+ await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
+ await bumpRoom(orange, app);
+ await checkOrder(["Orange", "Apple", "Pineapple", "Test Room"], page);
+ await bumpRoom(orange, app);
+ await checkOrder(["Orange", "Apple", "Pineapple", "Test Room"], page);
+ await bumpRoom(pineapple, app);
+ await checkOrder(["Pineapple", "Orange", "Apple", "Test Room"], page);
+ });
+
+ test.skip("should not move the selected room: it should be sticky", async ({ page, app }) => {
+ // create rooms and check room names are correct
+ const roomIds: string[] = [];
+ for (const fruit of ["Apple", "Pineapple", "Orange"]) {
+ const id = await app.client.createRoom({ name: fruit });
+ roomIds.push(id);
+ await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ }
+
+ // Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
+ // turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically
+ // be Apple, Orange Pineapple - only when you click on a different room do things reshuffle.
+
+ // Select the Pineapple room
+ await page.getByRole("treeitem", { name: "Pineapple" }).click();
+ await checkOrder(["Orange", "Pineapple", "Apple", "Test Room"], page);
+
+ // Move Apple
+ await bumpRoom(roomIds[0], app);
+ await checkOrder(["Apple", "Pineapple", "Orange", "Test Room"], page);
+
+ // Select the Test Room
+ await page.getByRole("treeitem", { name: "Test Room" }).click();
+
+ // the rooms reshuffle to match reality
+ await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
+ });
+
+ test.skip("should show the right unread notifications", async ({ page, app, user, bot }) => {
+ const bob = await createAndJoinBot(app, bot);
+
+ // send a message in the test room: unread notification count should increment
+ await bob.sendTextMessage(roomId, "Hello World");
+
+ const treeItemLocator1 = page.getByRole("treeitem", { name: "Test Room 1 unread message." });
+ await expect(treeItemLocator1.locator(".mx_NotificationBadge_count")).toHaveText("1");
+ // await expect(page.locator(".mx_NotificationBadge")).not.toHaveClass("mx_NotificationBadge_highlighted");
+ await expect(treeItemLocator1.locator(".mx_NotificationBadge")).not.toHaveClass(
+ /mx_NotificationBadge_highlighted/,
+ );
+
+ // send an @mention: highlight count (red) should be 2.
+ await bob.sendTextMessage(roomId, `Hello ${user.displayName}`);
+ const treeItemLocator2 = page.getByRole("treeitem", {
+ name: "Test Room 2 unread messages including mentions.",
+ });
+ await expect(treeItemLocator2.locator(".mx_NotificationBadge_count")).toHaveText("2");
+ await expect(treeItemLocator2.locator(".mx_NotificationBadge")).toHaveClass(/mx_NotificationBadge_highlighted/);
+
+ // click on the room, the notif counts should disappear
+ await page.getByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).click();
+ await expect(
+ page.getByRole("treeitem", { name: "Test Room" }).locator("mx_NotificationBadge_count"),
+ ).not.toBeAttached();
+ });
+
+ test.skip("should not show unread indicators", async ({ page, app, bot }) => {
+ // TODO: for now. Later we should.
+ await createAndJoinBot(app, bot);
+
+ // disable notifs in this room (TODO: CS API call?)
+ const locator = page.getByRole("treeitem", { name: "Test Room" });
+ await locator.hover();
+ await locator.getByRole("button", { name: "Notification options" }).click();
+ await page.getByRole("menuitemradio", { name: "Mute room" }).click();
+
+ // create a new room so we know when the message has been received as it'll re-shuffle the room list
+ await app.client.createRoom({ name: "Dummy" });
+
+ await checkOrder(["Dummy", "Test Room"], page);
+
+ await bot.sendTextMessage(roomId, "Do you read me?");
+
+ // wait for this message to arrive, tell by the room list resorting
+ await checkOrder(["Test Room", "Dummy"], page);
+
+ await expect(
+ page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"),
+ ).not.toBeAttached();
+ });
+
+ test("should update user settings promptly", async ({ page, app }) => {
+ await app.settings.openUserSettings("Preferences");
+ const locator = page.locator(".mx_SettingsFlag").filter({ hasText: "Show timestamps in 12 hour format" });
+ expect(locator).toBeVisible();
+ expect(locator.locator(".mx_ToggleSwitch_on")).not.toBeAttached();
+ await locator.locator(".mx_ToggleSwitch_ball").click();
+ expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached();
+ });
+
+ test.skip("should show and be able to accept/reject/rescind invites", async ({ page, app, bot }) => {
+ await createAndJoinBot(app, bot);
+
+ const clientUserId = await app.client.evaluate((client) => client.getUserId());
+
+ // invite bot into 3 rooms:
+ // - roomJoin: will join this room
+ // - roomReject: will reject the invite
+ // - roomRescind: will make Bob rescind the invite
+ const roomNames = ["Room to Join", "Room to Reject", "Room to Rescind"];
+ const roomRescind = await bot.evaluate(
+ async (client, { roomNames, clientUserId }) => {
+ const rooms = await Promise.all(roomNames.map((name) => client.createRoom({ name })));
+ await Promise.all(rooms.map((room) => client.invite(room.room_id, clientUserId)));
+ return rooms[2].room_id;
+ },
+ { roomNames, clientUserId },
+ );
+
+ await expect(
+ page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
+ ).toHaveCount(3);
+
+ // Select the room to join
+ await page.getByRole("treeitem", { name: "Room to Join" }).click();
+
+ // Accept the invite
+ await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
+
+ await checkOrder(["Room to Join", "Test Room"], page);
+
+ // Select the room to reject
+ await page.getByRole("treeitem", { name: "Room to Reject" }).click();
+
+ // Reject the invite
+ await page.locator(".mx_RoomView").getByRole("button", { name: "Reject", exact: true }).click();
+
+ await expect(
+ page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
+ ).toHaveCount(2);
+
+ // check the lists are correct
+ await checkOrder(["Room to Join", "Test Room"], page);
+
+ const titleLocator = page.getByRole("group", { name: "Invites" }).locator(".mx_RoomTile_title");
+ await expect(titleLocator).toHaveCount(1);
+ await expect(titleLocator).toHaveText("Room to Rescind");
+
+ // now rescind the invite
+ await bot.evaluate(
+ async (client, { roomRescind, clientUserId }) => {
+ client.kick(roomRescind, clientUserId);
+ },
+ { roomRescind, clientUserId },
+ );
+
+ // Wait for the rescind to take effect and check the joined list once more
+ await expect(
+ page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
+ ).toHaveCount(2);
+
+ await checkOrder(["Room to Join", "Test Room"], page);
+ });
+
+ test("should show a favourite DM only in the favourite sublist", async ({ page, app }) => {
+ const roomId = await app.client.createRoom({
+ name: "Favourite DM",
+ is_direct: true,
+ });
+ await app.client.evaluate(async (client, roomId) => {
+ client.setRoomTag(roomId, "m.favourite", { order: 0.5 });
+ }, roomId);
+ await expect(page.getByRole("group", { name: "Favourites" }).getByText("Favourite DM")).toBeVisible();
+ await expect(page.getByRole("group", { name: "People" }).getByText("Favourite DM")).not.toBeAttached();
+ });
+
+ // Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.
+ // This ensures we are setting RoomViewStore state correctly.
+ test.skip("should clear the reply to field when swapping rooms", async ({ page, app }) => {
+ await app.client.createRoom({ name: "Other Room" });
+ await expect(page.getByRole("treeitem", { name: "Other Room" })).toBeVisible();
+ await app.client.sendTextMessage(roomId, "Hello world");
+
+ // select the room
+ await page.getByRole("treeitem", { name: "Test Room" }).click();
+
+ await expect(page.locator(".mx_ReplyPreview")).not.toBeAttached();
+
+ // click reply-to on the Hello World message
+ const locator = page.locator(".mx_EventTile_last");
+ await locator.getByText("Hello world").hover();
+ await locator.getByRole("button", { name: "Reply", exact: true }).click({});
+
+ // check it's visible
+ await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
+
+ // now click Other Room
+ await page.getByRole("treeitem", { name: "Other Room" }).click();
+
+ // ensure the reply-to disappears
+ await expect(page.locator(".mx_ReplyPreview")).not.toBeAttached();
+
+ // click back
+ await page.getByRole("treeitem", { name: "Test Room" }).click();
+
+ // ensure the reply-to reappears
+ await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
+ });
+
+ // Regression test for https://github.com/vector-im/element-web/issues/21462
+ test.skip("should not cancel replies when permalinks are clicked", async ({ page, app }) => {
+ // we require a first message as you cannot click the permalink text with the avatar in the way
+ await app.client.sendTextMessage(roomId, "First message");
+ await app.client.sendTextMessage(roomId, "Permalink me");
+ await app.client.sendTextMessage(roomId, "Reply to me");
+
+ // select the room
+ await page.getByRole("treeitem", { name: "Test Room" }).click();
+ await expect(page.locator(".mx_ReplyPreview")).not.toBeAttached();
+
+ // click reply-to on the Reply to me message
+ const locator = page.locator(".mx_EventTile").last();
+ await locator.getByText("Reply to me").hover();
+ await locator.getByRole("button", { name: "Reply", exact: true }).click();
+
+ // check it's visible
+ await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
+
+ // now click on the permalink for Permalink me
+ await page.locator(".mx_EventTile").filter({ hasText: "Permalink me" }).locator("a").dispatchEvent("click");
+
+ // make sure it is now selected with the little green |
+ await expect(page.locator(".mx_EventTile_selected").filter({ hasText: "Permalink me" })).toBeVisible();
+
+ // ensure the reply-to does not disappear
+ await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
+ });
+
+ test.skip("should send unsubscribe_rooms for every room switch", async ({ page, app }) => {
+ // create rooms and check room names are correct
+ const roomIds: string[] = [];
+ for (const fruit of ["Apple", "Pineapple", "Orange"]) {
+ const id = await app.client.createRoom({ name: fruit });
+ roomIds.push(id);
+ await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ }
+ const [roomAId, roomPId] = roomIds;
+
+ const assertUnsubExists = (request: Request, subRoomId: string, unsubRoomId: string) => {
+ const body = request.postDataJSON();
+ // There may be a request without a txn_id, ignore it, as there won't be any subscription changes
+ if (body.txn_id === undefined) {
+ return;
+ }
+ expect(body.unsubscribe_rooms).toEqual([unsubRoomId]);
+ expect(body.room_subscriptions).not.toHaveProperty(unsubRoomId);
+ expect(body.room_subscriptions).toHaveProperty(subRoomId);
+ };
+
+ let promise = page.waitForRequest(/sync/);
+
+ // Select the Test Room
+ await page.getByRole("treeitem", { name: "Apple", exact: true }).click();
+
+ // and wait for playwright to get the request
+ const roomSubscriptions = (await promise).postDataJSON().room_subscriptions;
+ expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
+
+ // Switch to another room
+ promise = page.waitForRequest(/sync/);
+ await page.getByRole("treeitem", { name: "Pineapple", exact: true }).click();
+ assertUnsubExists(await promise, roomPId, roomAId);
+
+ // And switch to even another room
+ promise = page.waitForRequest(/sync/);
+ await page.getByRole("treeitem", { name: "Apple", exact: true }).click();
+ assertUnsubExists(await promise, roomPId, roomAId);
+
+ // TODO: Add tests for encrypted rooms
+ });
+});
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index d34406b54d..5a28518878 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -30,6 +30,7 @@ import { OAuthServer } from "./plugins/oauth_server";
import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts";
import { Bot, CreateBotOpts } from "./pages/bot";
+import { ProxyInstance, SlidingSyncProxy } from "./plugins/sliding-sync-proxy";
import { Webserver } from "./plugins/webserver";
const CONFIG_JSON: Partial = {
@@ -82,6 +83,7 @@ export const test = base.extend<
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
botCreateOpts: CreateBotOpts;
bot: Bot;
+ slidingSyncProxy: ProxyInstance;
labsFlags: string[];
webserver: Webserver;
}
@@ -104,7 +106,6 @@ export const test = base.extend<
}
await route.fulfill({ json });
});
-
await use(page);
},
@@ -180,7 +181,6 @@ export const test = base.extend<
{ baseUrl: homeserver.config.baseUrl, credentials },
);
await page.goto("/");
-
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
await use(credentials);
@@ -219,6 +219,25 @@ export const test = base.extend<
await use(bot);
},
+ slidingSyncProxy: async ({ page, user, homeserver }, use) => {
+ const proxy = new SlidingSyncProxy(homeserver.config.dockerUrl);
+ const proxyInstance = await proxy.start();
+ const proxyAddress = `http://localhost:${proxyInstance.port}`;
+ await page.addInitScript((proxyAddress) => {
+ window.localStorage.setItem(
+ "mx_local_settings",
+ JSON.stringify({
+ feature_sliding_sync_proxy_url: proxyAddress,
+ }),
+ );
+ window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true");
+ }, proxyAddress);
+ await page.goto("/");
+ await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
+ await use(proxyInstance);
+ await proxy.stop();
+ },
+
// eslint-disable-next-line no-empty-pattern
webserver: async ({}, use) => {
const webserver = new Webserver();
diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts
index 8bc0f5ae0e..1efc98d497 100644
--- a/playwright/pages/ElementAppPage.ts
+++ b/playwright/pages/ElementAppPage.ts
@@ -18,13 +18,11 @@ import { type Locator, type Page, expect } from "@playwright/test";
import { Settings } from "./settings";
import { Client } from "./client";
-import { Labs } from "./labs";
import { Spotlight } from "./Spotlight";
export class ElementAppPage {
public constructor(public readonly page: Page) {}
- public labs = new Labs(this.page);
public settings = new Settings(this.page);
public client: Client = new Client(this.page);
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index a197f09333..7f9180ca2e 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -101,7 +101,7 @@ export class Client {
}
/**
- * Send a message as a bot into a room
+ * Send a message into a room
* @param roomId ID of the room to send the message into
* @param content the event content to send
*/
@@ -134,6 +134,15 @@ export class Client {
);
}
+ /**
+ * Send a text message into a room
+ * @param roomId ID of the room to send the message into
+ * @param content the event content to send
+ */
+ public async sendTextMessage(roomId: string, message: string): Promise {
+ return await this.sendMessage(roomId, { msgtype: "m.text", body: message });
+ }
+
/**
* Create a room with given options.
* @param options the options to apply when creating the room
diff --git a/playwright/pages/labs.ts b/playwright/pages/labs.ts
deleted file mode 100644
index 55bce18225..0000000000
--- a/playwright/pages/labs.ts
+++ /dev/null
@@ -1,37 +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.
-*/
-
-import { Page } from "playwright-core";
-
-export class Labs {
- constructor(private page: Page) {}
-
- /**
- * Enables a labs feature for an element session.
- * @param feature labsFeature to enable (e.g. "feature_spotlight")
- */
- public async enableLabsFeature(feature: string): Promise {
- if (this.page.url() === "about:blank") {
- await this.page.addInitScript((feature) => {
- window.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
- }, feature);
- } else {
- await this.page.evaluate((feature) => {
- window.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
- }, feature);
- }
- }
-}
diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts
index a9823f5b05..2ca54cc0d8 100644
--- a/playwright/plugins/homeserver/dendrite/index.ts
+++ b/playwright/plugins/homeserver/dendrite/index.ts
@@ -74,9 +74,11 @@ export class Dendrite extends Synapse implements Homeserver, HomeserverInstance
"http://localhost:8008/_matrix/client/versions",
]);
+ const dockerUrl = `http://${await this.docker.getContainerIp()}:8008`;
this.config = {
...denCfg,
serverId: dendriteId,
+ dockerUrl,
};
return this;
}
@@ -107,7 +109,10 @@ export class Pinecone extends Dendrite {
protected entrypoint = "/usr/bin/dendrite-demo-pinecone";
}
-async function cfgDirFromTemplate(dendriteImage: string, opts: StartHomeserverOpts): Promise {
+async function cfgDirFromTemplate(
+ dendriteImage: string,
+ opts: StartHomeserverOpts,
+): Promise> {
const template = "default"; // XXX: for now we only have one template
const templateDir = path.join(__dirname, "templates", template);
diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts
index a4b9cdf98b..1e0cfb3b39 100644
--- a/playwright/plugins/homeserver/index.ts
+++ b/playwright/plugins/homeserver/index.ts
@@ -19,6 +19,7 @@ export interface HomeserverConfig {
readonly baseUrl: string;
readonly port: number;
readonly registrationSecret: string;
+ readonly dockerUrl: string;
}
export interface HomeserverInstance {
diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts
index ee789281ef..95165c1442 100644
--- a/playwright/plugins/homeserver/synapse/index.ts
+++ b/playwright/plugins/homeserver/synapse/index.ts
@@ -25,7 +25,7 @@ import { Docker } from "../../docker";
import { HomeserverConfig, HomeserverInstance, Homeserver, StartHomeserverOpts, Credentials } from "..";
import { randB64Bytes } from "../../utils/rand";
-async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise {
+async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> {
const templateDir = path.join(__dirname, "templates", opts.template);
const stats = await fse.stat(templateDir);
@@ -146,10 +146,11 @@ export class Synapse implements Homeserver, HomeserverInstance {
"--silent",
"http://localhost:8008/health",
]);
-
+ const dockerUrl = `http://${await this.docker.getContainerIp()}:8008`;
this.config = {
...synCfg,
serverId: synapseId,
+ dockerUrl,
};
return this;
}
diff --git a/playwright/plugins/sliding-sync-proxy/index.ts b/playwright/plugins/sliding-sync-proxy/index.ts
new file mode 100644
index 0000000000..649f0092d6
--- /dev/null
+++ b/playwright/plugins/sliding-sync-proxy/index.ts
@@ -0,0 +1,99 @@
+/*
+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 { getFreePort } from "../utils/port";
+import { Docker } from "../docker";
+
+// Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
+const SLIDING_SYNC_PROXY_TAG = "v0.99.3";
+const PG_PASSWORD = "p4S5w0rD";
+
+export interface ProxyInstance {
+ containerId: string;
+ postgresId: string;
+ port: number;
+}
+
+export class SlidingSyncProxy {
+ private readonly postgresDocker = new Docker();
+ private readonly proxyDocker = new Docker();
+ private instance: ProxyInstance;
+
+ constructor(private synapseIp: string) {}
+
+ private async waitForPostgresReady(): Promise {
+ const waitTimeMillis = 30000;
+ const startTime = new Date().getTime();
+ let lastErr: Error | null = null;
+ while (new Date().getTime() - startTime < waitTimeMillis) {
+ try {
+ await this.postgresDocker.exec(["pg_isready", "-U", "postgres"]);
+ lastErr = null;
+ break;
+ } catch (err) {
+ console.log("pg_isready: failed");
+ lastErr = err;
+ }
+ }
+ if (lastErr) {
+ console.log("rethrowing");
+ throw lastErr;
+ }
+ }
+
+ async start(): Promise {
+ console.log(new Date(), "Starting sliding sync proxy...");
+
+ const postgresId = await this.postgresDocker.run({
+ image: "postgres",
+ containerName: "react-sdk-cypress-sliding-sync-postgres",
+ params: ["--rm", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`],
+ });
+
+ const postgresIp = await this.postgresDocker.getContainerIp();
+ console.log(new Date(), "postgres container up");
+
+ await this.waitForPostgresReady();
+
+ const port = await getFreePort();
+ console.log(new Date(), "starting proxy container...", SLIDING_SYNC_PROXY_TAG);
+ const containerId = await this.proxyDocker.run({
+ image: "ghcr.io/matrix-org/sliding-sync:" + SLIDING_SYNC_PROXY_TAG,
+ containerName: "react-sdk-cypress-sliding-sync-proxy",
+ params: [
+ "--rm",
+ "-p",
+ `${port}:8008/tcp`,
+ "-e",
+ "SYNCV3_SECRET=bwahahaha",
+ "-e",
+ `SYNCV3_SERVER=${this.synapseIp}`,
+ "-e",
+ `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
+ ],
+ });
+ console.log(new Date(), "started!");
+
+ this.instance = { containerId, postgresId, port };
+ return this.instance;
+ }
+
+ async stop(): Promise {
+ await this.postgresDocker.stop();
+ await this.proxyDocker.stop();
+ console.log(new Date(), "Stopped sliding sync proxy.");
+ }
+}