Playwright: Convert sliding-sync test to playwright (#11989)
* Add method to send text message * Add dockerUrl to HomeServerConfig * Implement sliding sync proxy * Convert tests * Reload page after applying labs feature * Remove converted files * Remove timeout * Remove sliding-sync * Remove proxy import * Remove reference to proxy * wait for load * Update date * Convert enableLabsFeature to separate fixture * Enable feature in fixture * Skip over config and just write to local-storage * Rename fixture * Fix room header test * Use type inference * Override config instead of setting localstorage * Set default language * Always add labs feature * Put this one test into a separate describe block * Move labs lag within describe block
This commit is contained in:
parent
de5931d5a8
commit
7b3d5b5f21
16 changed files with 547 additions and 770 deletions
|
@ -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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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<HomeserverInstance>("@homeserver"), cy.get<ProxyInstance>("@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<HomeserverInstance>("@homeserver").then(cy.stopHomeserver);
|
||||
cy.get<ProxyInstance>("@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<string>(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<HomeserverInstance>("@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<string>("@roomId"), cy.get<MatrixClient>("@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<string>("@roomId"), cy.get<MatrixClient>("@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<string>("@roomId"), cy.get<MatrixClient>("@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<string>("@roomId"), cy.get<MatrixClient>("@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<MatrixClient>("@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<MatrixClient>("@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<string>("@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<string>("@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
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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<string, ProxyInstance>();
|
||||
|
||||
const PG_PASSWORD = "p4S5w0rD";
|
||||
|
||||
async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Promise<ProxyInstance> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -38,7 +38,6 @@ import "./iframes";
|
|||
import "./timeline";
|
||||
import "./network";
|
||||
import "./composer";
|
||||
import "./proxy";
|
||||
import "./axe";
|
||||
import "./promise";
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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<ProxyInstance>;
|
||||
|
||||
/**
|
||||
* 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<AUTWindow>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startProxy(homeserver: HomeserverInstance): Chainable<ProxyInstance> {
|
||||
return cy.task<ProxyInstance>("proxyStart", homeserver);
|
||||
}
|
||||
|
||||
function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
|
||||
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);
|
|
@ -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();
|
||||
|
|
|
@ -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,12 +137,14 @@ test.describe("Room Header", () => {
|
|||
await app.viewRoomByName("Test video room");
|
||||
};
|
||||
|
||||
test.describe("and with feature_notifications enabled", () => {
|
||||
test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
|
||||
|
||||
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);
|
||||
|
||||
const header = page.locator(".mx_LegacyRoomHeader");
|
||||
|
@ -170,6 +167,7 @@ test.describe("Room Header", () => {
|
|||
|
||||
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 ({
|
||||
page,
|
||||
|
|
375
playwright/e2e/sliding-sync/sliding-sync.spec.ts
Normal file
375
playwright/e2e/sliding-sync/sliding-sync.spec.ts
Normal file
|
@ -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<Bot> => {
|
||||
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
|
||||
});
|
||||
});
|
|
@ -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<IConfigOptions> = {
|
||||
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<ISendEventResponse> {
|
||||
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
|
||||
|
|
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<HomeserverConfig> {
|
||||
async function cfgDirFromTemplate(
|
||||
dendriteImage: string,
|
||||
opts: StartHomeserverOpts,
|
||||
): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||
const template = "default"; // XXX: for now we only have one template
|
||||
const templateDir = path.join(__dirname, "templates", template);
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface HomeserverConfig {
|
|||
readonly baseUrl: string;
|
||||
readonly port: number;
|
||||
readonly registrationSecret: string;
|
||||
readonly dockerUrl: string;
|
||||
}
|
||||
|
||||
export interface HomeserverInstance {
|
||||
|
|
|
@ -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<HomeserverConfig> {
|
||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||
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;
|
||||
}
|
||||
|
|
99
playwright/plugins/sliding-sync-proxy/index.ts
Normal file
99
playwright/plugins/sliding-sync-proxy/index.ts
Normal file
|
@ -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<void> {
|
||||
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<ProxyInstance> {
|
||||
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<void> {
|
||||
await this.postgresDocker.stop();
|
||||
await this.proxyDocker.stop();
|
||||
console.log(new Date(), "Stopped sliding sync proxy.");
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue