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",
|
||||
"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({});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
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 "./login";
|
||||
import "./client";
|
||||
import "./settings";
|
||||
import "./bot";
|
||||
|
|
|
@ -42,6 +42,15 @@ declare global {
|
|||
}
|
||||
|
||||
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 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,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
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
|
||||
return cy.window().then((win) => {
|
||||
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:
|
||||
|
||||
```
|
||||
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.
|
||||
|
|
|
@ -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",
|
||||
|
|
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 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<void>;
|
||||
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
|
||||
mxAutoRageshakeStore?: AutoRageshakeStore;
|
||||
mxDispatcher: MatrixDispatcher;
|
||||
}
|
||||
|
||||
interface Electron {
|
||||
|
|
|
@ -665,7 +665,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
|
|||
}
|
||||
|
||||
if (credentials.pickleKey) {
|
||||
let encryptedAccessToken;
|
||||
let encryptedAccessToken: IEncryptedPayload;
|
||||
try {
|
||||
// try to encrypt the access token using the pickle key
|
||||
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 {
|
||||
const doScroll = () => {
|
||||
if (!this.messagePanel.current) return;
|
||||
if (eventId) {
|
||||
debuglog("TimelinePanel scrolling to eventId " + eventId +
|
||||
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
|
||||
|
|
|
@ -67,9 +67,8 @@ export class MatrixDispatcher extends Dispatcher<ActionPayload> {
|
|||
|
||||
export const defaultDispatcher = new MatrixDispatcher();
|
||||
|
||||
const anyGlobal = <any>global;
|
||||
if (!anyGlobal.mxDispatcher) {
|
||||
anyGlobal.mxDispatcher = defaultDispatcher;
|
||||
if (!window.mxDispatcher) {
|
||||
window.mxDispatcher = defaultDispatcher;
|
||||
}
|
||||
|
||||
export default defaultDispatcher;
|
||||
|
|
|
@ -407,7 +407,7 @@ export class RoomViewStore extends Store<ActionPayload> {
|
|||
dis.dispatch({
|
||||
action: Action.JoinRoomError,
|
||||
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 { 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<void> {
|
||||
if (!indexedDB) {
|
||||
|
@ -206,8 +205,8 @@ async function idbInit(): Promise<void> {
|
|||
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(); };
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<ElementSession>,
|
||||
restCreator: RestSessionCreator): Promise<void> {
|
||||
|
@ -51,12 +49,6 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
|||
const alice = await createUser("alice");
|
||||
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 userViewScenarios(alice, bob);
|
||||
await roomDirectoryScenarios(alice, bob);
|
||||
|
@ -64,7 +56,6 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
|||
console.log("create REST users:");
|
||||
const charlies = await createRestUsers(restCreator);
|
||||
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
|
||||
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"
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue