diff --git a/cypress.json b/cypress.json
index 2c39bb411f..d41cc70dd0 100644
--- a/cypress.json
+++ b/cypress.json
@@ -1,5 +1,7 @@
{
"baseUrl": "http://localhost:8080",
"videoUploadOnPasses": false,
- "projectId": "ppvnzg"
+ "projectId": "ppvnzg",
+ "experimentalSessionAndOrigin": true,
+ "experimentalInteractiveRunEvents": true
}
diff --git a/cypress/global.d.ts b/cypress/global.d.ts
new file mode 100644
index 0000000000..d0fb732778
--- /dev/null
+++ b/cypress/global.d.ts
@@ -0,0 +1,45 @@
+/*
+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 "matrix-js-sdk/src/@types/global";
+import type { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
+import type { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
+import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface ApplicationWindow {
+ mxMatrixClientPeg: {
+ matrixClient?: MatrixClient;
+ };
+ mxDispatcher: MatrixDispatcher;
+ beforeReload?: boolean; // for detecting reloads
+ // Partial type for the matrix-js-sdk module, exported by browser-matrix
+ matrixcs: {
+ MatrixClient: typeof MatrixClient;
+ ClientEvent: typeof ClientEvent;
+ RoomMemberEvent: typeof RoomMemberEvent;
+ };
+ }
+ }
+
+ interface Window {
+ mxDispatcher: MatrixDispatcher; // to appease the MatrixDispatcher import
+ }
+}
+
+export { MatrixClient };
diff --git a/cypress/integration/2-login/consent.spec.ts b/cypress/integration/2-login/consent.spec.ts
new file mode 100644
index 0000000000..a4cd31bd26
--- /dev/null
+++ b/cypress/integration/2-login/consent.spec.ts
@@ -0,0 +1,73 @@
+/*
+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 { SinonStub } from "cypress/types/sinon";
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+
+describe("Consent", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.startSynapse("consent").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Bob");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should prompt the user to consent to terms when server deems it necessary", () => {
+ // Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN`
+ cy.window().then(win => {
+ win.mxMatrixClientPeg.matrixClient.createRoom({}).catch(() => {});
+
+ // Stub `window.open` - clicking the primary button below will call it
+ cy.stub(win, "open").as("windowOpen").returns({});
+ });
+
+ // Accept terms & conditions
+ cy.get(".mx_QuestionDialog").within(() => {
+ cy.get("#mx_BaseDialog_title").contains("Terms and Conditions");
+ cy.get(".mx_Dialog_primary").click();
+ });
+
+ cy.get("@windowOpen").then(stub => {
+ const url = stub.getCall(0).args[0];
+
+ // Go to Synapse's consent page and accept it
+ cy.origin(synapse.baseUrl, { args: { url } }, ({ url }) => {
+ cy.visit(url);
+
+ cy.get('[type="submit"]').click();
+ cy.get("p").contains("Danke schon");
+ });
+ });
+
+ // go back to the app
+ cy.visit("/");
+ // wait for the app to re-load
+ cy.get(".mx_MatrixChat", { timeout: 15000 });
+
+ // attempt to perform the same action again and expect it to not fail
+ cy.createRoom({});
+ });
+});
diff --git a/cypress/integration/3-user-menu/user-menu.spec.ts b/cypress/integration/3-user-menu/user-menu.spec.ts
index b3c482d9f1..671fd4eacf 100644
--- a/cypress/integration/3-user-menu/user-menu.spec.ts
+++ b/cypress/integration/3-user-menu/user-menu.spec.ts
@@ -19,12 +19,12 @@ limitations under the License.
import { SynapseInstance } from "../../plugins/synapsedocker";
import type { UserCredentials } from "../../support/login";
-describe("UserMenu", () => {
+describe("User Menu", () => {
let synapse: SynapseInstance;
let user: UserCredentials;
beforeEach(() => {
- cy.startSynapse("consent").then(data => {
+ cy.startSynapse("default").then(data => {
synapse = data;
cy.initTestUser(synapse, "Jeff").then(credentials => {
@@ -38,8 +38,10 @@ describe("UserMenu", () => {
});
it("should contain our name & userId", () => {
- cy.get('[aria-label="User menu"]', { timeout: 15000 }).click();
- cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
- cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
+ cy.get('[aria-label="User menu"]').click();
+ cy.get(".mx_ContextualMenu").within(() => {
+ cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
+ cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
+ });
});
});
diff --git a/cypress/integration/4-create-room/create-room.spec.ts b/cypress/integration/4-create-room/create-room.spec.ts
new file mode 100644
index 0000000000..d6abab814d
--- /dev/null
+++ b/cypress/integration/4-create-room/create-room.spec.ts
@@ -0,0 +1,64 @@
+/*
+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 { SynapseInstance } from "../../plugins/synapsedocker";
+import Chainable = Cypress.Chainable;
+
+function openCreateRoomDialog(): Chainable> {
+ cy.get('[aria-label="Add room"]').click();
+ cy.get('.mx_ContextualMenu [aria-label="New room"]').click();
+ return cy.get(".mx_CreateRoomDialog");
+}
+
+describe("Create Room", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Jim");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should allow us to create a public room with name, topic & address set", () => {
+ const name = "Test room 1";
+ const topic = "This room is dedicated to this test and this test only!";
+
+ openCreateRoomDialog().within(() => {
+ // Fill name & topic
+ cy.get('[label="Name"]').type(name);
+ cy.get('[label="Topic (optional)"]').type(topic);
+ // Change room to public
+ cy.get('[aria-label="Room visibility"]').click();
+ cy.get("#mx_JoinRuleDropdown__public").click();
+ // Fill room address
+ cy.get('[label="Room address"]').type("test-room-1");
+ // Submit
+ cy.get(".mx_Dialog_primary").click();
+ });
+
+ cy.url().should("contain", "/#/room/#test-room-1:localhost");
+ cy.get(".mx_RoomHeader_nametext").contains(name);
+ cy.get(".mx_RoomHeader_topic").contains(topic);
+ });
+});
diff --git a/cypress/integration/5-threads/threads.spec.ts b/cypress/integration/5-threads/threads.spec.ts
new file mode 100644
index 0000000000..43b0058bb1
--- /dev/null
+++ b/cypress/integration/5-threads/threads.spec.ts
@@ -0,0 +1,214 @@
+/*
+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 { SynapseInstance } from "../../plugins/synapsedocker";
+import { MatrixClient } from "../../global";
+
+function markWindowBeforeReload(): void {
+ // mark our window object to "know" when it gets reloaded
+ cy.window().then(w => w.beforeReload = true);
+}
+
+describe("Threads", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.window().then(win => {
+ win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
+ win.localStorage.setItem("mx_labs_feature_feature_thread", "true"); // Default threads to ON for this spec
+ });
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Tom");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should reload when enabling threads beta", () => {
+ markWindowBeforeReload();
+
+ // Turn off
+ cy.openUserSettings("Labs").within(() => {
+ // initially the new property is there
+ cy.window().should("have.prop", "beforeReload", true);
+
+ cy.leaveBeta("Threads");
+ // after reload the property should be gone
+ cy.window().should("not.have.prop", "beforeReload");
+ });
+
+ cy.get(".mx_MatrixChat", { timeout: 15000 }); // wait for the app
+ markWindowBeforeReload();
+
+ // Turn on
+ cy.openUserSettings("Labs").within(() => {
+ // initially the new property is there
+ cy.window().should("have.prop", "beforeReload", true);
+
+ cy.joinBeta("Threads");
+ // after reload the property should be gone
+ cy.window().should("not.have.prop", "beforeReload");
+ });
+ });
+
+ it("should be usable for a conversation", () => {
+ let bot: MatrixClient;
+ cy.getBot(synapse, "BotBob").then(_bot => {
+ bot = _bot;
+ });
+
+ let roomId: string;
+ cy.createRoom({}).then(_roomId => {
+ roomId = _roomId;
+ cy.inviteUser(roomId, bot.getUserId());
+ cy.visit("/#/room/" + roomId);
+ });
+
+ // User sends message
+ cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
+
+ // Wait for message to send, get its ID and save as @threadId
+ cy.get(".mx_RoomView_body .mx_EventTile").contains("Hello Mr. Bot")
+ .closest(".mx_EventTile[data-scroll-tokens]").invoke("attr", "data-scroll-tokens").as("threadId");
+
+ // Bot starts thread
+ cy.get("@threadId").then(threadId => {
+ bot.sendMessage(roomId, threadId, {
+ body: "Hello there",
+ msgtype: "m.text",
+ });
+ });
+
+ // User asserts timeline thread summary visible & clicks it
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
+
+ // User responds in thread
+ cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}");
+
+ // User asserts summary was updated correctly
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
+
+ // User reacts to message instead
+ cy.get(".mx_ThreadView .mx_EventTile").contains("Hello there").closest(".mx_EventTile_line")
+ .find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
+ cy.get(".mx_EmojiPicker").within(() => {
+ cy.get('input[type="text"]').type("wave");
+ cy.get('[role="menuitem"]').contains("👋").click();
+ });
+
+ // User redacts their prior response
+ cy.get(".mx_ThreadView .mx_EventTile").contains("Test").closest(".mx_EventTile_line")
+ .find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
+ cy.get(".mx_IconizedContextMenu").within(() => {
+ cy.get('[role="menuitem"]').contains("Remove").click();
+ });
+ cy.get(".mx_TextInputDialog").within(() => {
+ cy.get(".mx_Dialog_primary").contains("Remove").click();
+ });
+
+ // User asserts summary was updated correctly
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there");
+
+ // User closes right panel after clicking back to thread list
+ cy.get(".mx_ThreadView .mx_BaseCard_back").click();
+ cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
+
+ // Bot responds to thread
+ cy.get("@threadId").then(threadId => {
+ bot.sendMessage(roomId, threadId, {
+ body: "How are things?",
+ msgtype: "m.text",
+ });
+ });
+
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "How are things?");
+ // User asserts thread list unread indicator
+ cy.get('.mx_HeaderButtons [aria-label="Threads"]').should("have.class", "mx_RightPanel_headerButton_unread");
+
+ // User opens thread list
+ cy.get('.mx_HeaderButtons [aria-label="Threads"]').click();
+
+ // User asserts thread with correct root & latest events & unread dot
+ cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => {
+ cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot");
+ cy.get(".mx_ThreadSummary_content").should("contain", "How are things?");
+ // User opens thread via threads list
+ cy.get(".mx_EventTile_line").click();
+ });
+
+ // User responds & asserts
+ cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Great!{enter}");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Great!");
+
+ // User edits & asserts
+ cy.get(".mx_ThreadView .mx_EventTile_last").contains("Great!").closest(".mx_EventTile_line").within(() => {
+ cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
+ cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
+ });
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
+ .should("contain", "Great! How about yourself?");
+
+ // User closes right panel
+ cy.get(".mx_ThreadView .mx_BaseCard_close").click();
+
+ // Bot responds to thread and saves the id of their message to @eventId
+ cy.get("@threadId").then(threadId => {
+ cy.wrap(bot.sendMessage(roomId, threadId, {
+ body: "I'm very good thanks",
+ msgtype: "m.text",
+ }).then(res => res.event_id)).as("eventId");
+ });
+
+ // User asserts
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
+ .should("contain", "I'm very good thanks");
+
+ // Bot edits their latest event
+ cy.get("@eventId").then(eventId => {
+ bot.sendMessage(roomId, {
+ "body": "* I'm very good thanks :)",
+ "msgtype": "m.text",
+ "m.new_content": {
+ "body": "I'm very good thanks :)",
+ "msgtype": "m.text",
+ },
+ "m.relates_to": {
+ "rel_type": "m.replace",
+ "event_id": eventId,
+ },
+ });
+ });
+
+ // User asserts
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
+ .should("contain", "I'm very good thanks :)");
+ });
+});
diff --git a/cypress/plugins/synapsedocker/templates/default/README.md b/cypress/plugins/synapsedocker/templates/default/README.md
new file mode 100644
index 0000000000..8f6b11f999
--- /dev/null
+++ b/cypress/plugins/synapsedocker/templates/default/README.md
@@ -0,0 +1 @@
+A synapse configured with user privacy consent disabled
diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
new file mode 100644
index 0000000000..7839c69c46
--- /dev/null
+++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
@@ -0,0 +1,52 @@
+server_name: "localhost"
+pid_file: /data/homeserver.pid
+public_baseurl: "{{PUBLIC_BASEURL}}"
+listeners:
+ - port: 8008
+ tls: false
+ bind_addresses: ['::']
+ type: http
+ x_forwarded: true
+
+ resources:
+ - names: [client]
+ compress: false
+
+database:
+ name: "sqlite3"
+ args:
+ database: ":memory:"
+
+log_config: "/data/log.config"
+
+rc_messages_per_second: 10000
+rc_message_burst_count: 10000
+rc_registration:
+ per_second: 10000
+ burst_count: 10000
+
+rc_login:
+ address:
+ per_second: 10000
+ burst_count: 10000
+ account:
+ per_second: 10000
+ burst_count: 10000
+ failed_attempts:
+ per_second: 10000
+ burst_count: 10000
+
+media_store_path: "/data/media_store"
+uploads_path: "/data/uploads"
+enable_registration: true
+enable_registration_without_verification: true
+disable_msisdn_registration: false
+registration_shared_secret: "{{REGISTRATION_SECRET}}"
+report_stats: false
+macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
+form_secret: "{{FORM_SECRET}}"
+signing_key_path: "/data/localhost.signing.key"
+
+trusted_key_servers:
+ - server_name: "matrix.org"
+suppress_key_server_warning: true
diff --git a/cypress/plugins/synapsedocker/templates/default/log.config b/cypress/plugins/synapsedocker/templates/default/log.config
new file mode 100644
index 0000000000..ac232762da
--- /dev/null
+++ b/cypress/plugins/synapsedocker/templates/default/log.config
@@ -0,0 +1,50 @@
+# Log configuration for Synapse.
+#
+# This is a YAML file containing a standard Python logging configuration
+# dictionary. See [1] for details on the valid settings.
+#
+# Synapse also supports structured logging for machine readable logs which can
+# be ingested by ELK stacks. See [2] for details.
+#
+# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
+# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
+
+version: 1
+
+formatters:
+ precise:
+ format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
+
+handlers:
+ # A handler that writes logs to stderr. Unused by default, but can be used
+ # instead of "buffer" and "file" in the logger handlers.
+ console:
+ class: logging.StreamHandler
+ formatter: precise
+
+loggers:
+ synapse.storage.SQL:
+ # beware: increasing this to DEBUG will make synapse log sensitive
+ # information such as access tokens.
+ level: INFO
+
+ twisted:
+ # We send the twisted logging directly to the file handler,
+ # to work around https://github.com/matrix-org/synapse/issues/3471
+ # when using "buffer" logger. Use "console" to log to stderr instead.
+ handlers: [console]
+ propagate: false
+
+root:
+ level: INFO
+
+ # Write logs to the `buffer` handler, which will buffer them together in memory,
+ # then write them to a file.
+ #
+ # Replace "buffer" with "console" to log to stderr instead. (Note that you'll
+ # also need to update the configuration for the `twisted` logger above, in
+ # this case.)
+ #
+ handlers: [console]
+
+disable_existing_loggers: false
diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts
new file mode 100644
index 0000000000..3ba7b89cde
--- /dev/null
+++ b/cypress/support/bot.ts
@@ -0,0 +1,63 @@
+/*
+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 request from "browser-request";
+
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import { SynapseInstance } from "../plugins/synapsedocker";
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Returns a new Bot instance
+ * @param synapse the instance on which to register the bot user
+ * @param displayName the display name to give to the bot user
+ */
+ getBot(synapse: SynapseInstance, displayName?: string): Chainable;
+ }
+ }
+}
+
+Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string): Chainable => {
+ const username = Cypress._.uniqueId("userId_");
+ const password = Cypress._.uniqueId("password_");
+ return cy.registerUser(synapse, username, password, displayName).then(credentials => {
+ return cy.window().then(win => {
+ const cli = new win.matrixcs.MatrixClient({
+ baseUrl: synapse.baseUrl,
+ userId: credentials.userId,
+ deviceId: credentials.deviceId,
+ accessToken: credentials.accessToken,
+ request,
+ });
+
+ cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
+ if (member.membership === "invite" && member.userId === cli.getUserId()) {
+ cli.joinRoom(member.roomId);
+ }
+ });
+
+ cli.startClient();
+
+ return cli;
+ });
+ });
+});
diff --git a/cypress/support/client.ts b/cypress/support/client.ts
new file mode 100644
index 0000000000..eef3aa1086
--- /dev/null
+++ b/cypress/support/client.ts
@@ -0,0 +1,78 @@
+/*
+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 type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import type { Room } from "matrix-js-sdk/src/models/room";
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Returns the MatrixClient from the MatrixClientPeg
+ */
+ getClient(): Chainable;
+ /**
+ * Create a room with given options.
+ * @param options the options to apply when creating the room
+ * @return the ID of the newly created room
+ */
+ createRoom(options: ICreateRoomOpts): Chainable;
+ /**
+ * Invites the given user to the given room.
+ * @param roomId the id of the room to invite to
+ * @param userId the id of the user to invite
+ */
+ inviteUser(roomId: string, userId: string): Chainable<{}>;
+ }
+ }
+}
+
+Cypress.Commands.add("getClient", (): Chainable => {
+ return cy.window().then(win => win.mxMatrixClientPeg.matrixClient);
+});
+
+Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable => {
+ return cy.window().then(async win => {
+ const cli = win.mxMatrixClientPeg.matrixClient;
+ const resp = await cli.createRoom(options);
+ const roomId = resp.room_id;
+
+ if (!cli.getRoom(roomId)) {
+ await new Promise(resolve => {
+ const onRoom = (room: Room) => {
+ if (room.roomId === roomId) {
+ cli.off(win.matrixcs.ClientEvent.Room, onRoom);
+ resolve();
+ }
+ };
+ cli.on(win.matrixcs.ClientEvent.Room, onRoom);
+ });
+ }
+
+ return roomId;
+ });
+});
+
+Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => {
+ return cy.getClient().then(async (cli: MatrixClient) => {
+ return cli.invite(roomId, userId);
+ });
+});
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
index 598cc4de7e..6535d3a0b1 100644
--- a/cypress/support/index.ts
+++ b/cypress/support/index.ts
@@ -18,3 +18,6 @@ limitations under the License.
import "./synapse";
import "./login";
+import "./client";
+import "./settings";
+import "./bot";
diff --git a/cypress/support/login.ts b/cypress/support/login.ts
index 2d7d3ef84a..90e5c5dbdc 100644
--- a/cypress/support/login.ts
+++ b/cypress/support/login.ts
@@ -42,6 +42,15 @@ declare global {
}
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable => {
+ // XXX: work around Cypress not clearing IDB between tests
+ cy.window().then(win => {
+ win.indexedDB.databases().then(databases => {
+ databases.forEach(database => {
+ win.indexedDB.deleteDatabase(database.name);
+ });
+ });
+ });
+
const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_");
return cy.registerUser(synapse, username, password, displayName).then(() => {
@@ -64,7 +73,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
},
});
}).then(response => {
- return cy.window().then(win => {
+ cy.window().then(win => {
// Seed the localStorage with the required credentials
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
win.localStorage.setItem("mx_user_id", response.body.user_id);
@@ -73,14 +82,17 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
win.localStorage.setItem("mx_is_guest", "false");
win.localStorage.setItem("mx_has_pickle_key", "false");
win.localStorage.setItem("mx_has_access_token", "true");
-
- return cy.visit("/").then(() => ({
- password,
- accessToken: response.body.access_token,
- userId: response.body.user_id,
- deviceId: response.body.device_id,
- homeServer: response.body.home_server,
- }));
});
+
+ return cy.visit("/").then(() => {
+ // wait for the app to load
+ return cy.get(".mx_MatrixChat", { timeout: 15000 });
+ }).then(() => ({
+ password,
+ accessToken: response.body.access_token,
+ userId: response.body.user_id,
+ deviceId: response.body.device_id,
+ homeServer: response.body.home_server,
+ }));
});
});
diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts
new file mode 100644
index 0000000000..11f48c2db2
--- /dev/null
+++ b/cypress/support/settings.ts
@@ -0,0 +1,101 @@
+/*
+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 "./client"; // XXX: without an (any) import here, types break down
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Open the top left user menu, returning a handle to the resulting context menu.
+ */
+ openUserMenu(): Chainable>;
+
+ /**
+ * Open user settings (via user menu), returning a handle to the resulting dialog.
+ * @param tab the name of the tab to switch to after opening, optional.
+ */
+ openUserSettings(tab?: string): Chainable>;
+
+ /**
+ * Switch settings tab to the one by the given name, ideally call this in the context of the dialog.
+ * @param tab the name of the tab to switch to.
+ */
+ switchTabUserSettings(tab: string): Chainable>;
+
+ /**
+ * Close user settings, ideally call this in the context of the dialog.
+ */
+ closeUserSettings(): Chainable>;
+
+ /**
+ * Join the given beta, the `Labs` tab must already be opened,
+ * ideally call this in the context of the dialog.
+ * @param name the name of the beta to join.
+ */
+ joinBeta(name: string): Chainable>;
+
+ /**
+ * Leave the given beta, the `Labs` tab must already be opened,
+ * ideally call this in the context of the dialog.
+ * @param name the name of the beta to leave.
+ */
+ leaveBeta(name: string): Chainable>;
+ }
+ }
+}
+
+Cypress.Commands.add("openUserMenu", (): Chainable> => {
+ cy.get('[aria-label="User menu"]').click();
+ return cy.get(".mx_ContextualMenu");
+});
+
+Cypress.Commands.add("openUserSettings", (tab?: string): Chainable> => {
+ cy.openUserMenu().within(() => {
+ cy.get('[aria-label="All settings"]').click();
+ });
+ return cy.get(".mx_UserSettingsDialog").within(() => {
+ if (tab) {
+ cy.switchTabUserSettings(tab);
+ }
+ });
+});
+
+Cypress.Commands.add("switchTabUserSettings", (tab: string): Chainable> => {
+ return cy.get(".mx_TabbedView_tabLabels").within(() => {
+ cy.get(".mx_TabbedView_tabLabel").contains(tab).click();
+ });
+});
+
+Cypress.Commands.add("closeUserSettings", (): Chainable> => {
+ return cy.get('[aria-label="Close dialog"]').click();
+});
+
+Cypress.Commands.add("joinBeta", (name: string): Chainable> => {
+ return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
+ return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
+ });
+});
+
+Cypress.Commands.add("leaveBeta", (name: string): Chainable> => {
+ return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
+ return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
+ });
+});
diff --git a/cypress/support/synapse.ts b/cypress/support/synapse.ts
index 1571ddef36..aa1ba085f5 100644
--- a/cypress/support/synapse.ts
+++ b/cypress/support/synapse.ts
@@ -60,7 +60,8 @@ function startSynapse(template: string): Chainable {
return cy.task("synapseStart", template);
}
-function stopSynapse(synapse: SynapseInstance): Chainable {
+function stopSynapse(synapse?: SynapseInstance): Chainable {
+ if (!synapse) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window().then((win) => {
win.location.href = 'about:blank';
diff --git a/docs/cypress.md b/docs/cypress.md
index 95b9b330d1..d0a06bb163 100644
--- a/docs/cypress.md
+++ b/docs/cypress.md
@@ -31,7 +31,7 @@ This will run the Cypress tests once, non-interactively.
You can also run individual tests this way too, as you'd expect:
```
-yarn run test:cypress cypress/integration/1-register/register.spec.ts
+yarn run test:cypress --spec cypress/integration/1-register/register.spec.ts
```
Cypress also has its own UI that you can use to run and debug the tests.
@@ -131,12 +131,17 @@ but the signature can be maintained for simpler maintenance.
### Joining a Room
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
way to do this may be to get an access token for the user and use this to create a room with the REST
-API before logging the user in.
+API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
### Convenience APIs
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
creation that can be called to set up tests.
+### Using matrix-js-sdk
+Due to the way we run the Cypress tests in CI, at this time you can only use the matrix-js-sdk module
+exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
+This may be revisited in the future.
+
## Good Test Hygiene
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
already familiar with Cypress.
diff --git a/package.json b/package.json
index 7c9dac0b31..698780ad1c 100644
--- a/package.json
+++ b/package.json
@@ -166,7 +166,7 @@
"babel-jest": "^26.6.3",
"blob-polyfill": "^6.0.20211015",
"chokidar": "^3.5.1",
- "cypress": "^9.5.4",
+ "cypress": "^9.6.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"eslint": "8.9.0",
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index d0f266470c..4d87e0a2f0 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -51,6 +51,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
import { IConfigOptions } from "../IConfigOptions";
+import { MatrixDispatcher } from "../dispatcher/dispatcher";
/* eslint-disable @typescript-eslint/naming-convention */
@@ -109,6 +110,7 @@ declare global {
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise;
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise;
mxAutoRageshakeStore?: AutoRageshakeStore;
+ mxDispatcher: MatrixDispatcher;
}
interface Electron {
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 516e18ddc7..64cdf445d9 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -665,7 +665,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise {
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
const doScroll = () => {
+ if (!this.messagePanel.current) return;
if (eventId) {
debuglog("TimelinePanel scrolling to eventId " + eventId +
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
diff --git a/src/dispatcher/dispatcher.ts b/src/dispatcher/dispatcher.ts
index 15e2f8f9bc..4d4f83d4de 100644
--- a/src/dispatcher/dispatcher.ts
+++ b/src/dispatcher/dispatcher.ts
@@ -67,9 +67,8 @@ export class MatrixDispatcher extends Dispatcher {
export const defaultDispatcher = new MatrixDispatcher();
-const anyGlobal = global;
-if (!anyGlobal.mxDispatcher) {
- anyGlobal.mxDispatcher = defaultDispatcher;
+if (!window.mxDispatcher) {
+ window.mxDispatcher = defaultDispatcher;
}
export default defaultDispatcher;
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index 29e974498d..0cfe684441 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -407,7 +407,7 @@ export class RoomViewStore extends Store {
dis.dispatch({
action: Action.JoinRoomError,
roomId,
- err: err,
+ err,
});
}
}
diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts
index 7d9ce885f7..ed37064920 100644
--- a/src/utils/StorageManager.ts
+++ b/src/utils/StorageManager.ts
@@ -18,6 +18,7 @@ import { LocalStorageCryptoStore } from 'matrix-js-sdk/src/crypto/store/localSto
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import { logger } from "matrix-js-sdk/src/logger";
+import { MatrixClient } from 'matrix-js-sdk/src/client';
import Analytics from '../Analytics';
@@ -25,7 +26,7 @@ const localStorage = window.localStorage;
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
-let indexedDB;
+let indexedDB: IDBFactory;
try {
indexedDB = window.indexedDB;
} catch (e) {}
@@ -161,7 +162,7 @@ async function checkCryptoStore() {
track("Crypto store using IndexedDB inaccessible");
}
try {
- exists = await LocalStorageCryptoStore.exists(localStorage);
+ exists = LocalStorageCryptoStore.exists(localStorage);
log(`Crypto store using local storage contains data? ${exists}`);
return { exists, healthy: true };
} catch (e) {
@@ -172,12 +173,10 @@ async function checkCryptoStore() {
return { exists, healthy: false };
}
-export function trackStores(client) {
- if (client.store && client.store.on) {
- client.store.on("degraded", () => {
- track("Sync store using IndexedDB degraded to memory");
- });
- }
+export function trackStores(client: MatrixClient) {
+ client.store?.on?.("degraded", () => {
+ track("Sync store using IndexedDB degraded to memory");
+ });
}
/**
@@ -188,16 +187,16 @@ export function trackStores(client) {
* and if it is true and not crypto data is found, an error is
* presented to the user.
*
- * @param {bool} cryptoInited True if crypto has been set up
+ * @param {boolean} cryptoInited True if crypto has been set up
*/
-export function setCryptoInitialised(cryptoInited) {
- localStorage.setItem("mx_crypto_initialised", cryptoInited);
+export function setCryptoInitialised(cryptoInited: boolean) {
+ localStorage.setItem("mx_crypto_initialised", String(cryptoInited));
}
/* Simple wrapper functions around IndexedDB.
*/
-let idb = null;
+let idb: IDBDatabase = null;
async function idbInit(): Promise {
if (!indexedDB) {
@@ -206,8 +205,8 @@ async function idbInit(): Promise {
idb = await new Promise((resolve, reject) => {
const request = indexedDB.open("matrix-react-sdk", 1);
request.onerror = reject;
- request.onsuccess = (event) => { resolve(request.result); };
- request.onupgradeneeded = (event) => {
+ request.onsuccess = () => { resolve(request.result); };
+ request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore("pickleKey");
db.createObjectStore("account");
@@ -266,6 +265,6 @@ export async function idbDelete(
const objectStore = txn.objectStore(table);
const request = objectStore.delete(key);
request.onerror = reject;
- request.onsuccess = (event) => { resolve(); };
+ request.onsuccess = () => { resolve(); };
});
}
diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts
index 4ce762f36f..dc6e1309d7 100644
--- a/test/end-to-end-tests/src/scenario.ts
+++ b/test/end-to-end-tests/src/scenario.ts
@@ -30,8 +30,6 @@ import { stickerScenarios } from './scenarios/sticker';
import { userViewScenarios } from "./scenarios/user-view";
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
import { updateScenarios } from "./scenarios/update";
-import { threadsScenarios } from "./scenarios/threads";
-import { enableThreads } from "./usecases/threads";
export async function scenario(createSession: (s: string) => Promise,
restCreator: RestSessionCreator): Promise {
@@ -51,12 +49,6 @@ export async function scenario(createSession: (s: string) => Promise Promise {
- console.log(" threads tests:");
-
- // Alice sends message
- await sendMessage(alice, "Hey bob, what do you think about X?");
-
- // Bob responds via a thread
- await startThread(bob, "I think its Y!");
-
- // Alice sees thread summary and opens thread panel
- await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
- await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
- await clickTimelineThreadSummary(alice);
-
- // Bob closes right panel
- await closeRoomRightPanel(bob);
-
- // Alice responds in thread
- await sendThreadMessage(alice, "Great!");
- await assertTimelineThreadSummary(alice, "alice", "Great!");
- await assertTimelineThreadSummary(bob, "alice", "Great!");
-
- // Alice reacts to Bob's message instead
- await reactThreadMessage(alice, "😁");
- await assertTimelineThreadSummary(alice, "alice", "Great!");
- await assertTimelineThreadSummary(bob, "alice", "Great!");
- await redactThreadMessage(alice);
- await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
- await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
-
- // Bob sees notification dot on the thread header icon
- await assertThreadListHasUnreadIndicator(bob);
-
- // Bob opens thread list and inspects it
- await openThreadListPanel(bob);
-
- // Bob opens thread in right panel via thread list
- await clickLatestThreadInThreadListPanel(bob);
-
- // Bob responds to thread
- await sendThreadMessage(bob, "Testing threads s'more :)");
- await assertTimelineThreadSummary(alice, "bob", "Testing threads s'more :)");
- await assertTimelineThreadSummary(bob, "bob", "Testing threads s'more :)");
-
- // Bob edits thread response
- await editThreadMessage(bob, "Testing threads some more :)");
- await assertTimelineThreadSummary(alice, "bob", "Testing threads some more :)");
- await assertTimelineThreadSummary(bob, "bob", "Testing threads some more :)");
-}
diff --git a/test/end-to-end-tests/src/usecases/threads.ts b/test/end-to-end-tests/src/usecases/threads.ts
deleted file mode 100644
index d263f147df..0000000000
--- a/test/end-to-end-tests/src/usecases/threads.ts
+++ /dev/null
@@ -1,153 +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 { strict as assert } from "assert";
-
-import { ElementSession } from "../session";
-
-export async function enableThreads(session: ElementSession): Promise {
- session.log.step(`enables threads`);
- await session.page.evaluate(() => {
- window["mxSettingsStore"].setValue("feature_thread", null, "device", true);
- });
- session.log.done();
-}
-
-async function clickReplyInThread(session: ElementSession): Promise {
- const events = await session.queryAll(".mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
- const button = await event.$(".mx_MessageActionBar_threadButton");
- await button.click();
-}
-
-export async function sendThreadMessage(session: ElementSession, message: string): Promise {
- session.log.step(`sends thread response "${message}"`);
- const composer = await session.query(".mx_ThreadView .mx_BasicMessageComposer_input");
- await composer.click();
- await composer.type(message);
-
- const text = await session.innerText(composer);
- assert.equal(text.trim(), message.trim());
- await composer.press("Enter");
- // wait for the message to appear sent
- await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
- session.log.done();
-}
-
-export async function editThreadMessage(session: ElementSession, message: string): Promise {
- session.log.step(`edits thread response "${message}"`);
- const events = await session.queryAll(".mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
- const button = await event.$(".mx_MessageActionBar_editButton");
- await button.click();
-
- const composer = await session.query(".mx_ThreadView .mx_EditMessageComposer .mx_BasicMessageComposer_input");
- await composer.click({ clickCount: 3 });
- await composer.type(message);
-
- const text = await session.innerText(composer);
- assert.equal(text.trim(), message.trim());
- await composer.press("Enter");
- // wait for the edit to appear sent
- await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
- session.log.done();
-}
-
-export async function redactThreadMessage(session: ElementSession): Promise {
- session.log.startGroup(`redacts latest thread response`);
-
- const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
-
- session.log.step(`clicks the ... button`);
- let button = await event.$('.mx_MessageActionBar [aria-label="Options"]');
- await button.click();
- session.log.done();
-
- session.log.step(`clicks the remove option`);
- button = await session.query('.mx_IconizedContextMenu_item[aria-label="Remove"]');
- await button.click();
- session.log.done();
-
- session.log.step(`confirms in the dialog`);
- button = await session.query(".mx_Dialog_primary");
- await button.click();
- session.log.done();
-
- await session.query(".mx_ThreadView .mx_RedactedBody");
- await session.delay(1000); // give the app a chance to settle
-
- session.log.endGroup();
-}
-
-export async function reactThreadMessage(session: ElementSession, reaction: string): Promise {
- session.log.startGroup(`reacts to latest thread response`);
-
- const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
-
- session.log.step(`clicks the reaction button`);
- let button = await event.$('.mx_MessageActionBar [aria-label="React"]');
- await button.click();
- session.log.done();
-
- session.log.step(`selects reaction`);
- button = await session.query(`.mx_EmojiPicker_item_wrapper[aria-label=${reaction}]`);
- await button.click;
- session.log.done();
-
- session.log.step(`clicks away`);
- button = await session.query(".mx_ContextualMenu_background");
- await button.click();
- session.log.done();
-
- session.log.endGroup();
-}
-
-export async function startThread(session: ElementSession, response: string): Promise {
- session.log.startGroup(`creates thread on latest message`);
-
- await clickReplyInThread(session);
- await sendThreadMessage(session, response);
-
- session.log.endGroup();
-}
-
-export async function assertTimelineThreadSummary(
- session: ElementSession,
- sender: string,
- content: string,
-): Promise {
- session.log.step("asserts the timeline thread summary is as expected");
- const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadSummary");
- const summary = summaries[summaries.length - 1];
- assert.equal(await session.innerText(await summary.$(".mx_ThreadSummary_sender")), sender);
- assert.equal(await session.innerText(await summary.$(".mx_ThreadSummary_content")), content);
- session.log.done();
-}
-
-export async function clickTimelineThreadSummary(session: ElementSession): Promise {
- session.log.step(`clicks the latest thread summary in the timeline`);
-
- const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadSummary");
- await summaries[summaries.length - 1].click();
-
- session.log.done();
-}
diff --git a/yarn.lock b/yarn.lock
index c1f274ecf4..1a7053bba9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3352,10 +3352,10 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
-cypress@^9.5.4:
- version "9.6.0"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.0.tgz#84473b3362255fa8f5e627a596e58575c9e5320f"
- integrity sha512-nNwt9eBQmSENamwa8LxvggXksfyzpyYaQ7lNBLgks3XZ6dPE/6BCQFBzeAyAPt/bNXfH3tKPkAyhiAZPYkWoEg==
+cypress@^9.6.1:
+ version "9.6.1"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb"
+ integrity sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==
dependencies:
"@cypress/request" "^2.88.10"
"@cypress/xvfb" "^1.2.4"