Move threads e2e tests over to cypress (#8501)
* Add non-consent (default) Synapse template * Add consent test * Add create room test * Stash work * Initial threads tests * fix * Delete old threads e2e tests, plan new ones * Fix typed s'more * Try something else * specify d.ts * Fix types once and for all? * Fix the consent tests * Iterate threads test harness * Fix dispatcher types * Iterate threads test * fix typing * Alternative import attempt * let it break let it break let it break * Tweak types * Stash * delint and update docs * null-guard scrollIntoView * Iterate threads test * Apply suggestions from code review
This commit is contained in:
parent
14127c777b
commit
ad4d3f9a88
27 changed files with 810 additions and 288 deletions
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:8080",
|
"baseUrl": "http://localhost:8080",
|
||||||
"videoUploadOnPasses": false,
|
"videoUploadOnPasses": false,
|
||||||
"projectId": "ppvnzg"
|
"projectId": "ppvnzg",
|
||||||
|
"experimentalSessionAndOrigin": true,
|
||||||
|
"experimentalInteractiveRunEvents": true
|
||||||
}
|
}
|
||||||
|
|
45
cypress/global.d.ts
vendored
Normal file
45
cypress/global.d.ts
vendored
Normal file
|
@ -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 };
|
73
cypress/integration/2-login/consent.spec.ts
Normal file
73
cypress/integration/2-login/consent.spec.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<SinonStub>("@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({});
|
||||||
|
});
|
||||||
|
});
|
|
@ -24,7 +24,7 @@ describe("UserMenu", () => {
|
||||||
let user: UserCredentials;
|
let user: UserCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("consent").then(data => {
|
cy.startSynapse("default").then(data => {
|
||||||
synapse = data;
|
synapse = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Jeff").then(credentials => {
|
cy.initTestUser(synapse, "Jeff").then(credentials => {
|
||||||
|
@ -38,8 +38,10 @@ describe("UserMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should contain our name & userId", () => {
|
it("should contain our name & userId", () => {
|
||||||
cy.get('[aria-label="User menu"]', { timeout: 15000 }).click();
|
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_displayName").should("contain", "Jeff");
|
||||||
cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
|
cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
64
cypress/integration/4-create-room/create-room.spec.ts
Normal file
64
cypress/integration/4-create-room/create-room.spec.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
function openCreateRoomDialog(): Chainable<JQuery<HTMLElement>> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
214
cypress/integration/5-threads/threads.spec.ts
Normal file
214
cypress/integration/5-threads/threads.spec.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<string>("@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<string>("@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<string>("@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<string>("@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 :)");
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
A synapse configured with user privacy consent disabled
|
|
@ -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
|
50
cypress/plugins/synapsedocker/templates/default/log.config
Normal file
50
cypress/plugins/synapsedocker/templates/default/log.config
Normal file
|
@ -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
|
63
cypress/support/bot.ts
Normal file
63
cypress/support/bot.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<MatrixClient>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string): Chainable<MatrixClient> => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
78
cypress/support/client.ts
Normal file
78
cypress/support/client.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<MatrixClient | undefined>;
|
||||||
|
/**
|
||||||
|
* 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<string>;
|
||||||
|
/**
|
||||||
|
* 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<MatrixClient | undefined> => {
|
||||||
|
return cy.window().then(win => win.mxMatrixClientPeg.matrixClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
|
||||||
|
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<void>(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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,3 +18,6 @@ limitations under the License.
|
||||||
|
|
||||||
import "./synapse";
|
import "./synapse";
|
||||||
import "./login";
|
import "./login";
|
||||||
|
import "./client";
|
||||||
|
import "./settings";
|
||||||
|
import "./bot";
|
||||||
|
|
|
@ -42,6 +42,15 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
|
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
|
||||||
|
// 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 username = Cypress._.uniqueId("userId_");
|
||||||
const password = Cypress._.uniqueId("password_");
|
const password = Cypress._.uniqueId("password_");
|
||||||
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
||||||
|
@ -64,7 +73,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
return cy.window().then(win => {
|
cy.window().then(win => {
|
||||||
// Seed the localStorage with the required credentials
|
// Seed the localStorage with the required credentials
|
||||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||||
win.localStorage.setItem("mx_user_id", response.body.user_id);
|
win.localStorage.setItem("mx_user_id", response.body.user_id);
|
||||||
|
@ -73,8 +82,12 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
||||||
win.localStorage.setItem("mx_is_guest", "false");
|
win.localStorage.setItem("mx_is_guest", "false");
|
||||||
win.localStorage.setItem("mx_has_pickle_key", "false");
|
win.localStorage.setItem("mx_has_pickle_key", "false");
|
||||||
win.localStorage.setItem("mx_has_access_token", "true");
|
win.localStorage.setItem("mx_has_access_token", "true");
|
||||||
|
});
|
||||||
|
|
||||||
return cy.visit("/").then(() => ({
|
return cy.visit("/").then(() => {
|
||||||
|
// wait for the app to load
|
||||||
|
return cy.get(".mx_MatrixChat", { timeout: 15000 });
|
||||||
|
}).then(() => ({
|
||||||
password,
|
password,
|
||||||
accessToken: response.body.access_token,
|
accessToken: response.body.access_token,
|
||||||
userId: response.body.user_id,
|
userId: response.body.user_id,
|
||||||
|
@ -83,4 +96,3 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
101
cypress/support/settings.ts
Normal file
101
cypress/support/settings.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<JQuery<HTMLElement>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<JQuery<HTMLElement>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<JQuery<HTMLElement>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close user settings, ideally call this in the context of the dialog.
|
||||||
|
*/
|
||||||
|
closeUserSettings(): Chainable<JQuery<HTMLElement>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<JQuery<HTMLElement>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<JQuery<HTMLElement>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("openUserMenu", (): Chainable<JQuery<HTMLElement>> => {
|
||||||
|
cy.get('[aria-label="User menu"]').click();
|
||||||
|
return cy.get(".mx_ContextualMenu");
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("openUserSettings", (tab?: string): Chainable<JQuery<HTMLElement>> => {
|
||||||
|
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<JQuery<HTMLElement>> => {
|
||||||
|
return cy.get(".mx_TabbedView_tabLabels").within(() => {
|
||||||
|
cy.get(".mx_TabbedView_tabLabel").contains(tab).click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("closeUserSettings", (): Chainable<JQuery<HTMLElement>> => {
|
||||||
|
return cy.get('[aria-label="Close dialog"]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("joinBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||||
|
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<JQuery<HTMLElement>> => {
|
||||||
|
return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
|
||||||
|
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
|
||||||
|
});
|
||||||
|
});
|
|
@ -60,7 +60,8 @@ function startSynapse(template: string): Chainable<SynapseInstance> {
|
||||||
return cy.task<SynapseInstance>("synapseStart", template);
|
return cy.task<SynapseInstance>("synapseStart", template);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow> {
|
function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
|
||||||
|
if (!synapse) return;
|
||||||
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
||||||
return cy.window().then((win) => {
|
return cy.window().then((win) => {
|
||||||
win.location.href = 'about:blank';
|
win.location.href = 'about:blank';
|
||||||
|
|
|
@ -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:
|
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.
|
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
|
### Joining a Room
|
||||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
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
|
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
|
### Convenience APIs
|
||||||
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
|
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.
|
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
|
## Good Test Hygiene
|
||||||
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
|
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
|
||||||
already familiar with Cypress.
|
already familiar with Cypress.
|
||||||
|
|
|
@ -166,7 +166,7 @@
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"blob-polyfill": "^6.0.20211015",
|
"blob-polyfill": "^6.0.20211015",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"cypress": "^9.5.4",
|
"cypress": "^9.6.1",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-to-json": "^3.6.2",
|
"enzyme-to-json": "^3.6.2",
|
||||||
"eslint": "8.9.0",
|
"eslint": "8.9.0",
|
||||||
|
|
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -51,6 +51,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
||||||
import { IConfigOptions } from "../IConfigOptions";
|
import { IConfigOptions } from "../IConfigOptions";
|
||||||
|
import { MatrixDispatcher } from "../dispatcher/dispatcher";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
@ -109,6 +110,7 @@ declare global {
|
||||||
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
||||||
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
|
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
|
||||||
mxAutoRageshakeStore?: AutoRageshakeStore;
|
mxAutoRageshakeStore?: AutoRageshakeStore;
|
||||||
|
mxDispatcher: MatrixDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Electron {
|
interface Electron {
|
||||||
|
|
|
@ -665,7 +665,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentials.pickleKey) {
|
if (credentials.pickleKey) {
|
||||||
let encryptedAccessToken;
|
let encryptedAccessToken: IEncryptedPayload;
|
||||||
try {
|
try {
|
||||||
// try to encrypt the access token using the pickle key
|
// try to encrypt the access token using the pickle key
|
||||||
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
|
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
|
||||||
|
|
|
@ -1197,6 +1197,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
|
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
|
||||||
const doScroll = () => {
|
const doScroll = () => {
|
||||||
|
if (!this.messagePanel.current) return;
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
debuglog("TimelinePanel scrolling to eventId " + eventId +
|
debuglog("TimelinePanel scrolling to eventId " + eventId +
|
||||||
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
|
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
|
||||||
|
|
|
@ -67,9 +67,8 @@ export class MatrixDispatcher extends Dispatcher<ActionPayload> {
|
||||||
|
|
||||||
export const defaultDispatcher = new MatrixDispatcher();
|
export const defaultDispatcher = new MatrixDispatcher();
|
||||||
|
|
||||||
const anyGlobal = <any>global;
|
if (!window.mxDispatcher) {
|
||||||
if (!anyGlobal.mxDispatcher) {
|
window.mxDispatcher = defaultDispatcher;
|
||||||
anyGlobal.mxDispatcher = defaultDispatcher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultDispatcher;
|
export default defaultDispatcher;
|
||||||
|
|
|
@ -407,7 +407,7 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.JoinRoomError,
|
action: Action.JoinRoomError,
|
||||||
roomId,
|
roomId,
|
||||||
err: err,
|
err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { LocalStorageCryptoStore } from 'matrix-js-sdk/src/crypto/store/localSto
|
||||||
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
||||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
import Analytics from '../Analytics';
|
import Analytics from '../Analytics';
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ const localStorage = window.localStorage;
|
||||||
|
|
||||||
// just *accessing* indexedDB throws an exception in firefox with
|
// just *accessing* indexedDB throws an exception in firefox with
|
||||||
// indexeddb disabled.
|
// indexeddb disabled.
|
||||||
let indexedDB;
|
let indexedDB: IDBFactory;
|
||||||
try {
|
try {
|
||||||
indexedDB = window.indexedDB;
|
indexedDB = window.indexedDB;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
@ -161,7 +162,7 @@ async function checkCryptoStore() {
|
||||||
track("Crypto store using IndexedDB inaccessible");
|
track("Crypto store using IndexedDB inaccessible");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
exists = await LocalStorageCryptoStore.exists(localStorage);
|
exists = LocalStorageCryptoStore.exists(localStorage);
|
||||||
log(`Crypto store using local storage contains data? ${exists}`);
|
log(`Crypto store using local storage contains data? ${exists}`);
|
||||||
return { exists, healthy: true };
|
return { exists, healthy: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -172,13 +173,11 @@ async function checkCryptoStore() {
|
||||||
return { exists, healthy: false };
|
return { exists, healthy: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trackStores(client) {
|
export function trackStores(client: MatrixClient) {
|
||||||
if (client.store && client.store.on) {
|
client.store?.on?.("degraded", () => {
|
||||||
client.store.on("degraded", () => {
|
|
||||||
track("Sync store using IndexedDB degraded to memory");
|
track("Sync store using IndexedDB degraded to memory");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether crypto has ever been successfully
|
* Sets whether crypto has ever been successfully
|
||||||
|
@ -188,16 +187,16 @@ export function trackStores(client) {
|
||||||
* and if it is true and not crypto data is found, an error is
|
* and if it is true and not crypto data is found, an error is
|
||||||
* presented to the user.
|
* 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) {
|
export function setCryptoInitialised(cryptoInited: boolean) {
|
||||||
localStorage.setItem("mx_crypto_initialised", cryptoInited);
|
localStorage.setItem("mx_crypto_initialised", String(cryptoInited));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Simple wrapper functions around IndexedDB.
|
/* Simple wrapper functions around IndexedDB.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let idb = null;
|
let idb: IDBDatabase = null;
|
||||||
|
|
||||||
async function idbInit(): Promise<void> {
|
async function idbInit(): Promise<void> {
|
||||||
if (!indexedDB) {
|
if (!indexedDB) {
|
||||||
|
@ -206,8 +205,8 @@ async function idbInit(): Promise<void> {
|
||||||
idb = await new Promise((resolve, reject) => {
|
idb = await new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open("matrix-react-sdk", 1);
|
const request = indexedDB.open("matrix-react-sdk", 1);
|
||||||
request.onerror = reject;
|
request.onerror = reject;
|
||||||
request.onsuccess = (event) => { resolve(request.result); };
|
request.onsuccess = () => { resolve(request.result); };
|
||||||
request.onupgradeneeded = (event) => {
|
request.onupgradeneeded = () => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
db.createObjectStore("pickleKey");
|
db.createObjectStore("pickleKey");
|
||||||
db.createObjectStore("account");
|
db.createObjectStore("account");
|
||||||
|
@ -266,6 +265,6 @@ export async function idbDelete(
|
||||||
const objectStore = txn.objectStore(table);
|
const objectStore = txn.objectStore(table);
|
||||||
const request = objectStore.delete(key);
|
const request = objectStore.delete(key);
|
||||||
request.onerror = reject;
|
request.onerror = reject;
|
||||||
request.onsuccess = (event) => { resolve(); };
|
request.onsuccess = () => { resolve(); };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,6 @@ import { stickerScenarios } from './scenarios/sticker';
|
||||||
import { userViewScenarios } from "./scenarios/user-view";
|
import { userViewScenarios } from "./scenarios/user-view";
|
||||||
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
|
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
|
||||||
import { updateScenarios } from "./scenarios/update";
|
import { updateScenarios } from "./scenarios/update";
|
||||||
import { threadsScenarios } from "./scenarios/threads";
|
|
||||||
import { enableThreads } from "./usecases/threads";
|
|
||||||
|
|
||||||
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
||||||
restCreator: RestSessionCreator): Promise<void> {
|
restCreator: RestSessionCreator): Promise<void> {
|
||||||
|
@ -51,12 +49,6 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
const alice = await createUser("alice");
|
const alice = await createUser("alice");
|
||||||
const bob = await createUser("bob");
|
const bob = await createUser("bob");
|
||||||
|
|
||||||
// Enable threads for Alice & Bob before going any further as it requires refreshing the app
|
|
||||||
// which otherwise loses all performance ticks.
|
|
||||||
console.log("Enabling threads: ");
|
|
||||||
await enableThreads(alice);
|
|
||||||
await enableThreads(bob);
|
|
||||||
|
|
||||||
await toastScenarios(alice, bob);
|
await toastScenarios(alice, bob);
|
||||||
await userViewScenarios(alice, bob);
|
await userViewScenarios(alice, bob);
|
||||||
await roomDirectoryScenarios(alice, bob);
|
await roomDirectoryScenarios(alice, bob);
|
||||||
|
@ -64,7 +56,6 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
console.log("create REST users:");
|
console.log("create REST users:");
|
||||||
const charlies = await createRestUsers(restCreator);
|
const charlies = await createRestUsers(restCreator);
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
await lazyLoadingScenarios(alice, bob, charlies);
|
||||||
await threadsScenarios(alice, bob);
|
|
||||||
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
|
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
|
||||||
await spacesScenarios(alice, bob);
|
await spacesScenarios(alice, bob);
|
||||||
|
|
||||||
|
|
|
@ -1,83 +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 { ElementSession } from "../session";
|
|
||||||
import {
|
|
||||||
assertTimelineThreadSummary,
|
|
||||||
clickTimelineThreadSummary,
|
|
||||||
editThreadMessage,
|
|
||||||
reactThreadMessage,
|
|
||||||
redactThreadMessage,
|
|
||||||
sendThreadMessage,
|
|
||||||
startThread,
|
|
||||||
} from "../usecases/threads";
|
|
||||||
import { sendMessage } from "../usecases/send-message";
|
|
||||||
import {
|
|
||||||
assertThreadListHasUnreadIndicator,
|
|
||||||
clickLatestThreadInThreadListPanel,
|
|
||||||
closeRoomRightPanel,
|
|
||||||
openThreadListPanel,
|
|
||||||
} from "../usecases/rightpanel";
|
|
||||||
|
|
||||||
export async function threadsScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
|
|
||||||
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 :)");
|
|
||||||
}
|
|
|
@ -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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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();
|
|
||||||
}
|
|
|
@ -3352,10 +3352,10 @@ csstype@^3.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
|
||||||
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
|
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
|
||||||
|
|
||||||
cypress@^9.5.4:
|
cypress@^9.6.1:
|
||||||
version "9.6.0"
|
version "9.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.0.tgz#84473b3362255fa8f5e627a596e58575c9e5320f"
|
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb"
|
||||||
integrity sha512-nNwt9eBQmSENamwa8LxvggXksfyzpyYaQ7lNBLgks3XZ6dPE/6BCQFBzeAyAPt/bNXfH3tKPkAyhiAZPYkWoEg==
|
integrity sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cypress/request" "^2.88.10"
|
"@cypress/request" "^2.88.10"
|
||||||
"@cypress/xvfb" "^1.2.4"
|
"@cypress/xvfb" "^1.2.4"
|
||||||
|
|
Loading…
Reference in a new issue