diff --git a/cypress/e2e/lazy-loading/lazy-loading.spec.ts b/cypress/e2e/lazy-loading/lazy-loading.spec.ts
new file mode 100644
index 0000000000..af4d7ef6ae
--- /dev/null
+++ b/cypress/e2e/lazy-loading/lazy-loading.spec.ts
@@ -0,0 +1,174 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import { MatrixClient } from "../../global";
+import Chainable = Cypress.Chainable;
+
+interface Charly {
+ client: MatrixClient;
+ displayName: string;
+}
+
+describe("Lazy Loading", () => {
+ let synapse: SynapseInstance;
+ let bob: MatrixClient;
+ const charlies: Charly[] = [];
+
+ beforeEach(() => {
+ cy.window().then(win => {
+ win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
+ });
+
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Alice");
+
+ cy.getBot(synapse, {
+ displayName: "Bob",
+ startClient: false,
+ autoAcceptInvites: false,
+ }).then(_bob => {
+ bob = _bob;
+ });
+
+ for (let i = 1; i <= 10; i++) {
+ const displayName = `Charly #${i}`;
+ cy.getBot(synapse, {
+ displayName,
+ startClient: false,
+ autoAcceptInvites: false,
+ }).then(client => {
+ charlies[i - 1] = { displayName, client };
+ });
+ }
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ const name = "Lazy Loading Test";
+ const alias = "#lltest:localhost";
+ const charlyMsg1 = "hi bob!";
+ const charlyMsg2 = "how's it going??";
+
+ function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
+ cy.window({ log: false }).then(win => {
+ return cy.wrap(bob.createRoom({
+ name,
+ room_alias_name: "lltest",
+ visibility: win.matrixcs.Visibility.Public,
+ }).then(r => r.room_id), { log: false }).as("roomId");
+ });
+
+ cy.get("@roomId").then(async roomId => {
+ for (const charly of charlies) {
+ await charly.client.joinRoom(alias);
+ }
+
+ for (const charly of charlies) {
+ cy.botSendMessage(charly.client, roomId, charlyMsg1);
+ }
+ for (const charly of charlies) {
+ cy.botSendMessage(charly.client, roomId, charlyMsg2);
+ }
+
+ for (let i = 20; i >= 1; --i) {
+ cy.botSendMessage(bob, roomId, `I will only say this ${i} time(s)!`);
+ }
+ });
+
+ cy.joinRoom(alias);
+ cy.viewRoomByName(name);
+ }
+
+ function checkPaginatedDisplayNames(charlies: Charly[]) {
+ cy.scrollToTop();
+ for (const charly of charlies) {
+ cy.findEventTile(charly.displayName, charlyMsg1).should("exist");
+ cy.findEventTile(charly.displayName, charlyMsg2).should("exist");
+ }
+ }
+
+ function openMemberlist(): void {
+ cy.get('.mx_HeaderButtons [aria-label="Room Info"]').click();
+ cy.get(".mx_RoomSummaryCard").within(() => {
+ cy.get(".mx_RoomSummaryCard_icon_people").click();
+ });
+ }
+
+ function getMembersInMemberlist(): Chainable {
+ return cy.get(".mx_MemberList .mx_EntityTile_name");
+ }
+
+ function checkMemberList(charlies: Charly[]) {
+ getMembersInMemberlist().contains("Alice").should("exist");
+ getMembersInMemberlist().contains("Bob").should("exist");
+ charlies.forEach(charly => {
+ getMembersInMemberlist().contains(charly.displayName).should("exist");
+ });
+ }
+
+ function checkMemberListLacksCharlies(charlies: Charly[]) {
+ charlies.forEach(charly => {
+ getMembersInMemberlist().contains(charly.displayName).should("not.exist");
+ });
+ }
+
+ function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
+ cy.goOffline();
+
+ cy.get("@roomId").then(async roomId => {
+ for (const charly of charlies) {
+ await charly.client.joinRoom(alias);
+ }
+ for (let i = 20; i >= 1; --i) {
+ cy.botSendMessage(charlies[0].client, roomId, "where is charly?");
+ }
+ });
+
+ cy.goOnline();
+ cy.wait(1000); // Ideally we'd await a /sync here but intercepts step on each other from going offline/online
+ }
+
+ it("should handle lazy loading properly even when offline", () => {
+ const charly1to5 = charlies.slice(0, 5);
+ const charly6to10 = charlies.slice(5);
+
+ // Set up room with alice, bob & charlies 1-5
+ setupRoomWithBobAliceAndCharlies(charly1to5);
+ // Alice should see 2 messages from every charly with the correct display name
+ checkPaginatedDisplayNames(charly1to5);
+
+ openMemberlist();
+ checkMemberList(charly1to5);
+ joinCharliesWhileAliceIsOffline(charly6to10);
+ checkMemberList(charly6to10);
+
+ cy.get("@roomId").then(async roomId => {
+ for (const charly of charlies) {
+ await charly.client.leave(roomId);
+ }
+ });
+
+ checkMemberListLacksCharlies(charlies);
+ });
+});
diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts
index 91efca9ea0..f724d6b3d3 100644
--- a/cypress/support/bot.ts
+++ b/cypress/support/bot.ts
@@ -18,7 +18,7 @@ limitations under the License.
import request from "browser-request";
-import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
+import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { SynapseInstance } from "../plugins/synapsedocker";
import Chainable = Cypress.Chainable;
@@ -31,10 +31,15 @@ interface CreateBotOpts {
* The display name to give to that bot user
*/
displayName?: string;
+ /**
+ * Whether or not to start the syncing client.
+ */
+ startClient?: boolean;
}
const defaultCreateBotOptions = {
autoAcceptInvites: true,
+ startClient: true,
} as CreateBotOpts;
declare global {
@@ -59,6 +64,13 @@ declare global {
* @param roomName Name of the room to join
*/
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable;
+ /**
+ * Send a message as a bot into a room
+ * @param cli The bot's MatrixClient
+ * @param roomId ID of the room to join
+ * @param message the message body to send
+ */
+ botSendMessage(cli: MatrixClient, roomId: string, message: string): Chainable;
}
}
}
@@ -88,6 +100,10 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
});
}
+ if (!opts.startClient) {
+ return cy.wrap(cli);
+ }
+
return cy.wrap(
cli.initCrypto()
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
@@ -114,3 +130,14 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
return cy.wrap(Promise.reject());
});
+
+Cypress.Commands.add("botSendMessage", (
+ cli: MatrixClient,
+ roomId: string,
+ message: string,
+): Chainable => {
+ return cy.wrap(cli.sendMessage(roomId, {
+ msgtype: "m.text",
+ body: message,
+ }), { log: false });
+});
diff --git a/cypress/support/client.ts b/cypress/support/client.ts
index 8f9b14e851..c3f3aab0eb 100644
--- a/cypress/support/client.ts
+++ b/cypress/support/client.ts
@@ -124,6 +124,11 @@ declare global {
* Boostraps cross-signing.
*/
bootstrapCrossSigning(): Chainable;
+ /**
+ * Joins the given room by alias or ID
+ * @param roomIdOrAlias the id or alias of the room to join
+ */
+ joinRoom(roomIdOrAlias: string): Chainable;
}
}
}
@@ -217,3 +222,7 @@ Cypress.Commands.add("bootstrapCrossSigning", () => {
});
});
});
+
+Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable => {
+ return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
+});
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
index 4a0852c64a..18445d8d04 100644
--- a/cypress/support/e2e.ts
+++ b/cypress/support/e2e.ts
@@ -33,3 +33,5 @@ import "./percy";
import "./webserver";
import "./views";
import "./iframes";
+import "./timeline";
+import "./network";
diff --git a/cypress/support/network.ts b/cypress/support/network.ts
new file mode 100644
index 0000000000..73df049c6c
--- /dev/null
+++ b/cypress/support/network.ts
@@ -0,0 +1,62 @@
+/*
+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.
+*/
+
+///
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ // Intercept all /_matrix/ networking requests for the logged in user and fail them
+ goOffline(): void;
+ // Remove intercept on all /_matrix/ networking requests
+ goOnline(): void;
+ }
+ }
+}
+
+// We manage intercepting Matrix APIs here, as fully disabling networking will disconnect
+// the browser under test from the Cypress runner, so can cause issues.
+
+Cypress.Commands.add("goOffline", (): void => {
+ cy.log("Going offline");
+ cy.window({ log: false }).then(win => {
+ cy.intercept("**/_matrix/**", {
+ headers: {
+ "Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
+ },
+ }, req => {
+ req.destroy();
+ });
+ });
+});
+
+Cypress.Commands.add("goOnline", (): void => {
+ cy.log("Going online");
+ cy.window({ log: false }).then(win => {
+ cy.intercept("**/_matrix/**", {
+ headers: {
+ "Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
+ },
+ }, req => {
+ req.continue();
+ });
+ win.dispatchEvent(new Event("online"));
+ });
+});
+
+// Needed to make this file a module
+export { };
diff --git a/cypress/support/timeline.ts b/cypress/support/timeline.ts
new file mode 100644
index 0000000000..28a9705fdb
--- /dev/null
+++ b/cypress/support/timeline.ts
@@ -0,0 +1,68 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ // Scroll to the top of the timeline
+ scrollToTop(): void;
+ // Find the event tile matching the given sender & body
+ findEventTile(sender: string, body: string): Chainable;
+ }
+ }
+}
+
+export interface Message {
+ sender: string;
+ body: string;
+ encrypted: boolean;
+ continuation: boolean;
+}
+
+Cypress.Commands.add("scrollToTop", (): void => {
+ cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
+ if (ref.scrollTop() > 0) {
+ return cy.scrollToTop();
+ }
+ });
+});
+
+Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable => {
+ // We can't just use a bunch of `.contains` here due to continuations meaning that the events don't
+ // have their own rendered sender displayname so we have to walk the list to keep track of the sender.
+ return cy.get(".mx_RoomView_MessageList .mx_EventTile").then(refs => {
+ let latestSender: string;
+ for (let i = 0; i < refs.length; i++) {
+ const ref = refs.eq(i);
+ const displayName = ref.find(".mx_DisambiguatedProfile_displayName");
+ if (displayName) {
+ latestSender = displayName.text();
+ }
+
+ if (latestSender === sender && ref.find(".mx_EventTile_body").text() === body) {
+ return ref;
+ }
+ }
+ });
+});
+
+// Needed to make this file a module
+export { };
diff --git a/package.json b/package.json
index e40d2d672c..be5976ffd5 100644
--- a/package.json
+++ b/package.json
@@ -172,7 +172,7 @@
"blob-polyfill": "^6.0.20211015",
"chokidar": "^3.5.1",
"cypress": "^10.3.0",
- "cypress-real-events": "^1.7.0",
+ "cypress-real-events": "^1.7.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"eslint": "8.9.0",
diff --git a/test/end-to-end-tests/src/rest/consent.ts b/test/end-to-end-tests/src/rest/consent.ts
deleted file mode 100644
index 8b2e19821c..0000000000
--- a/test/end-to-end-tests/src/rest/consent.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import request = require('request-promise-native');
-import * as cheerio from 'cheerio';
-import * as url from "url";
-
-export const approveConsent = async function(consentUrl: string): Promise {
- const body = await request.get(consentUrl);
- const doc = cheerio.load(body);
- const v = doc("input[name=v]").val();
- const u = doc("input[name=u]").val();
- const h = doc("input[name=h]").val();
- const formAction = doc("form").attr("action");
- const absAction = url.resolve(consentUrl, formAction);
- await request.post(absAction).form({ v, u, h });
-};
diff --git a/test/end-to-end-tests/src/rest/creator.ts b/test/end-to-end-tests/src/rest/creator.ts
deleted file mode 100644
index 33eea675d9..0000000000
--- a/test/end-to-end-tests/src/rest/creator.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import request = require('request-promise-native');
-import * as crypto from 'crypto';
-
-import { RestSession } from './session';
-import { RestMultiSession } from './multi';
-
-export interface Credentials {
- accessToken: string;
- homeServer: string;
- userId: string;
- deviceId: string;
- hsUrl: string;
-}
-
-export class RestSessionCreator {
- constructor(private readonly hsUrl: string, private readonly regSecret: string) {}
-
- public async createSessionRange(usernames: string[], password: string,
- groupName: string): Promise {
- const sessionPromises = usernames.map((username) => this.createSession(username, password));
- const sessions = await Promise.all(sessionPromises);
- return new RestMultiSession(sessions, groupName);
- }
-
- public async createSession(username: string, password: string): Promise {
- await this.register(username, password);
- console.log(` * created REST user ${username} ... done`);
- const authResult = await this.authenticate(username, password);
- return new RestSession(authResult);
- }
-
- private async register(username: string, password: string): Promise {
- // get a nonce
- const regUrl = `${this.hsUrl}/_synapse/admin/v1/register`;
- const nonceResp = await request.get({ uri: regUrl, json: true });
-
- const mac = crypto.createHmac('sha1', this.regSecret).update(
- `${nonceResp.nonce}\0${username}\0${password}\0notadmin`,
- ).digest('hex');
-
- await request.post({
- uri: regUrl,
- json: true,
- body: {
- nonce: nonceResp.nonce,
- username,
- password,
- mac,
- admin: false,
- },
- });
- }
-
- private async authenticate(username: string, password: string): Promise {
- const requestBody = {
- "type": "m.login.password",
- "identifier": {
- "type": "m.id.user",
- "user": username,
- },
- "password": password,
- };
- const url = `${this.hsUrl}/_matrix/client/r0/login`;
- const responseBody = await request.post({ url, json: true, body: requestBody });
- return {
- accessToken: responseBody.access_token,
- homeServer: responseBody.home_server,
- userId: responseBody.user_id,
- deviceId: responseBody.device_id,
- hsUrl: this.hsUrl,
- };
- }
-}
diff --git a/test/end-to-end-tests/src/rest/multi.ts b/test/end-to-end-tests/src/rest/multi.ts
deleted file mode 100644
index 00f127567f..0000000000
--- a/test/end-to-end-tests/src/rest/multi.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 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 { Logger } from '../logger';
-import { RestSession } from "./session";
-import { RestRoom } from "./room";
-
-export class RestMultiSession {
- readonly log: Logger;
-
- constructor(public readonly sessions: RestSession[], groupName: string) {
- this.log = new Logger(groupName);
- }
-
- public slice(groupName: string, start: number, end?: number): RestMultiSession {
- return new RestMultiSession(this.sessions.slice(start, end), groupName);
- }
-
- public pop(userName: string): RestSession {
- const idx = this.sessions.findIndex((s) => s.userName() === userName);
- if (idx === -1) {
- throw new Error(`user ${userName} not found`);
- }
- const session = this.sessions.splice(idx, 1)[0];
- return session;
- }
-
- public async setDisplayName(fn: (s: RestSession) => string): Promise {
- this.log.step("set their display name");
- await Promise.all(this.sessions.map(async (s: RestSession) => {
- s.log.mute();
- await s.setDisplayName(fn(s));
- s.log.unmute();
- }));
- this.log.done();
- }
-
- public async join(roomIdOrAlias: string): Promise {
- this.log.step(`join ${roomIdOrAlias}`);
- const rooms = await Promise.all(this.sessions.map(async (s) => {
- s.log.mute();
- const room = await s.join(roomIdOrAlias);
- s.log.unmute();
- return room;
- }));
- this.log.done();
- return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
- }
-
- public room(roomIdOrAlias: string): RestMultiRoom {
- const rooms = this.sessions.map(s => s.room(roomIdOrAlias));
- return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
- }
-}
-
-class RestMultiRoom {
- constructor(public readonly rooms: RestRoom[], private readonly roomIdOrAlias: string,
- private readonly log: Logger) {}
-
- public async talk(message: string): Promise {
- this.log.step(`say "${message}" in ${this.roomIdOrAlias}`);
- await Promise.all(this.rooms.map(async (r: RestRoom) => {
- r.log.mute();
- await r.talk(message);
- r.log.unmute();
- }));
- this.log.done();
- }
-
- public async leave() {
- this.log.step(`leave ${this.roomIdOrAlias}`);
- await Promise.all(this.rooms.map(async (r) => {
- r.log.mute();
- await r.leave();
- r.log.unmute();
- }));
- this.log.done();
- }
-}
diff --git a/test/end-to-end-tests/src/rest/room.ts b/test/end-to-end-tests/src/rest/room.ts
deleted file mode 100644
index 2261f95993..0000000000
--- a/test/end-to-end-tests/src/rest/room.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 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 uuidv4 = require('uuid/v4');
-
-import { RestSession } from "./session";
-import { Logger } from "../logger";
-
-/* no pun intended */
-export class RestRoom {
- constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
-
- async talk(message: string): Promise {
- this.log.step(`says "${message}" in ${this.roomId}`);
- const txId = uuidv4();
- const { event_id: eventId } = await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
- "msgtype": "m.text",
- "body": message,
- });
- this.log.done();
- return eventId;
- }
-
- async leave(): Promise {
- this.log.step(`leaves ${this.roomId}`);
- await this.session.post(`/rooms/${this.roomId}/leave`);
- this.log.done();
- }
-}
diff --git a/test/end-to-end-tests/src/rest/session.ts b/test/end-to-end-tests/src/rest/session.ts
deleted file mode 100644
index a6536ac3a6..0000000000
--- a/test/end-to-end-tests/src/rest/session.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import request = require('request-promise-native');
-
-import { Logger } from '../logger';
-import { RestRoom } from './room';
-import { approveConsent } from './consent';
-import { Credentials } from "./creator";
-
-interface RoomOptions {
- invite?: string;
- public?: boolean;
- topic?: string;
- dm?: boolean;
-}
-
-export class RestSession {
- private _displayName: string = null;
- private readonly rooms: Record = {};
- readonly log: Logger;
-
- constructor(private readonly credentials: Credentials) {
- this.log = new Logger(credentials.userId);
- }
-
- userId(): string {
- return this.credentials.userId;
- }
-
- userName(): string {
- return this.credentials.userId.split(":")[0].slice(1);
- }
-
- displayName(): string {
- return this._displayName;
- }
-
- async setDisplayName(displayName: string): Promise {
- this.log.step(`sets their display name to ${displayName}`);
- this._displayName = displayName;
- await this.put(`/profile/${this.credentials.userId}/displayname`, {
- displayname: displayName,
- });
- this.log.done();
- }
-
- async join(roomIdOrAlias: string): Promise {
- this.log.step(`joins ${roomIdOrAlias}`);
- const roomId = (await this.post(`/join/${encodeURIComponent(roomIdOrAlias)}`)).room_id;
- this.log.done();
- const room = new RestRoom(this, roomId, this.log);
- this.rooms[roomId] = room;
- this.rooms[roomIdOrAlias] = room;
- return room;
- }
-
- room(roomIdOrAlias: string): RestRoom {
- if (this.rooms.hasOwnProperty(roomIdOrAlias)) {
- return this.rooms[roomIdOrAlias];
- } else {
- throw new Error(`${this.credentials.userId} is not in ${roomIdOrAlias}`);
- }
- }
-
- async createRoom(name: string, options: RoomOptions): Promise {
- this.log.step(`creates room ${name}`);
- const body = {
- name,
- };
- if (options.invite) {
- body['invite'] = options.invite;
- }
- if (options.public) {
- body['visibility'] = "public";
- } else {
- body['visibility'] = "private";
- }
- if (options.dm) {
- body['is_direct'] = true;
- }
- if (options.topic) {
- body['topic'] = options.topic;
- }
-
- const roomId = (await this.post(`/createRoom`, body)).room_id;
- this.log.done();
- return new RestRoom(this, roomId, this.log);
- }
-
- post(csApiPath: string, body?: any): Promise {
- return this.request("POST", csApiPath, body);
- }
-
- put(csApiPath: string, body?: any): Promise {
- return this.request("PUT", csApiPath, body);
- }
-
- async request(method: string, csApiPath: string, body?: any): Promise {
- try {
- return await request({
- url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`,
- method,
- headers: {
- "Authorization": `Bearer ${this.credentials.accessToken}`,
- },
- json: true,
- body,
- });
- } catch (err) {
- if (!err.response) {
- throw err;
- }
- const responseBody = err.response.body;
- if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') {
- await approveConsent(responseBody.consent_uri);
- return this.request(method, csApiPath, body);
- } else if (responseBody && responseBody.error) {
- throw new Error(`${method} ${csApiPath}: ${responseBody.error}`);
- } else {
- throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`);
- }
- }
- }
-}
diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts
index 1c81205e27..6c6495fef1 100644
--- a/test/end-to-end-tests/src/scenario.ts
+++ b/test/end-to-end-tests/src/scenario.ts
@@ -15,18 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { range } from './util';
import { signup } from './usecases/signup';
import { toastScenarios } from './scenarios/toast';
-import { lazyLoadingScenarios } from './scenarios/lazy-loading';
import { e2eEncryptionScenarios } from './scenarios/e2e-encryption';
import { ElementSession } from "./session";
-import { RestSessionCreator } from "./rest/creator";
-import { RestMultiSession } from "./rest/multi";
-import { RestSession } from "./rest/session";
-export async function scenario(createSession: (s: string) => Promise,
- restCreator: RestSessionCreator): Promise {
+export async function scenario(createSession: (s: string) => Promise): Promise {
let firstUser = true;
async function createUser(username: string) {
const session = await createSession(username);
@@ -45,14 +39,4 @@ export async function scenario(createSession: (s: string) => Promise {
- const usernames = range(1, 10).map((i) => `charly-${i}`);
- const charlies = await restCreator.createSessionRange(usernames, "testtest", "charly-1..10");
- await charlies.setDisplayName((s: RestSession) => `Charly #${s.userName().split('-')[1]}`);
- return charlies;
}
diff --git a/test/end-to-end-tests/src/scenarios/lazy-loading.ts b/test/end-to-end-tests/src/scenarios/lazy-loading.ts
deleted file mode 100644
index 3cbfdafdab..0000000000
--- a/test/end-to-end-tests/src/scenarios/lazy-loading.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 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 { delay } from '../util';
-import { join } from '../usecases/join';
-import { sendMessage } from '../usecases/send-message';
-import {
- checkTimelineContains,
- scrollToTimelineTop,
-} from '../usecases/timeline';
-import { createRoom } from '../usecases/create-room';
-import { getMembersInMemberlist } from '../usecases/memberlist';
-import { changeRoomSettings } from '../usecases/room-settings';
-import { RestMultiSession } from "../rest/multi";
-import { ElementSession } from "../session";
-
-export async function lazyLoadingScenarios(alice: ElementSession,
- bob: ElementSession, charlies: RestMultiSession): Promise {
- console.log(" creating a room for lazy loading member scenarios:");
- const charly1to5 = charlies.slice("charly-1..5", 0, 5);
- const charly6to10 = charlies.slice("charly-6..10", 5);
- assert(charly1to5.sessions.length == 5);
- assert(charly6to10.sessions.length == 5);
- await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5);
- await checkPaginatedDisplayNames(alice, charly1to5);
- await checkMemberList(alice, charly1to5);
- await joinCharliesWhileAliceIsOffline(alice, charly6to10);
- await checkMemberList(alice, charly6to10);
- await charlies.room(alias).leave();
- await delay(1000);
- await checkMemberListLacksCharlies(alice, charlies);
- await checkMemberListLacksCharlies(bob, charlies);
-}
-
-const room = "Lazy Loading Test";
-const alias = "#lltest:localhost";
-const charlyMsg1 = "hi bob!";
-const charlyMsg2 = "how's it going??";
-
-async function setupRoomWithBobAliceAndCharlies(alice: ElementSession, bob: ElementSession,
- charlies: RestMultiSession): Promise {
- await createRoom(bob, room);
- await changeRoomSettings(bob, { directory: true, visibility: "public", alias });
- // wait for alias to be set by server after clicking "save"
- // so the charlies can join it.
- await bob.delay(500);
- const charlyMembers = await charlies.join(alias);
- await charlyMembers.talk(charlyMsg1);
- await charlyMembers.talk(charlyMsg2);
- bob.log.step("sends 20 messages").mute();
- for (let i = 20; i >= 1; --i) {
- await sendMessage(bob, `I will only say this ${i} time(s)!`);
- }
- bob.log.unmute().done();
- await join(alice, alias);
-}
-
-async function checkPaginatedDisplayNames(alice: ElementSession, charlies: RestMultiSession): Promise {
- await scrollToTimelineTop(alice);
- //alice should see 2 messages from every charly with
- //the correct display name
- const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => {
- return charlies.sessions.reduce((messages, charly) => {
- return messages.concat({
- sender: charly.displayName(),
- body: msgText,
- });
- }, messages);
- }, []);
- await checkTimelineContains(alice, expectedMessages, charlies.log.username);
-}
-
-async function checkMemberList(alice: ElementSession, charlies: RestMultiSession): Promise {
- alice.log.step(`checks the memberlist contains herself, bob and ${charlies.log.username}`);
- const displayNames = (await getMembersInMemberlist(alice)).map((m) => m.displayName);
- assert(displayNames.includes("alice"));
- assert(displayNames.includes("bob"));
- charlies.sessions.forEach((charly) => {
- assert(displayNames.includes(charly.displayName()),
- `${charly.displayName()} should be in the member list, ` +
- `only have ${displayNames}`);
- });
- alice.log.done();
-}
-
-async function checkMemberListLacksCharlies(session: ElementSession, charlies: RestMultiSession): Promise {
- session.log.step(`checks the memberlist doesn't contain ${charlies.log.username}`);
- const displayNames = (await getMembersInMemberlist(session)).map((m) => m.displayName);
- charlies.sessions.forEach((charly) => {
- assert(!displayNames.includes(charly.displayName()),
- `${charly.displayName()} should not be in the member list, ` +
- `only have ${displayNames}`);
- });
- session.log.done();
-}
-
-async function joinCharliesWhileAliceIsOffline(alice: ElementSession, charly6to10: RestMultiSession) {
- await alice.setOffline(true);
- await delay(1000);
- const members6to10 = await charly6to10.join(alias);
- const member6 = members6to10.rooms[0];
- member6.log.step("sends 20 messages").mute();
- for (let i = 20; i >= 1; --i) {
- await member6.talk("where is charly?");
- }
- member6.log.unmute().done();
- const catchupPromise = alice.waitForNextSuccessfulSync();
- await alice.setOffline(false);
- await catchupPromise;
- await delay(2000);
-}
diff --git a/test/end-to-end-tests/src/session.ts b/test/end-to-end-tests/src/session.ts
index c3f19db532..9f8d67bae9 100644
--- a/test/end-to-end-tests/src/session.ts
+++ b/test/end-to-end-tests/src/session.ts
@@ -118,24 +118,6 @@ export class ElementSession {
return await this.page.$$(selector);
}
- /** wait for a /sync request started after this call that gets a 200 response */
- public async waitForNextSuccessfulSync(): Promise {
- const syncUrls = [];
- function onRequest(request) {
- if (request.url().indexOf("/sync") !== -1) {
- syncUrls.push(request.url());
- }
- }
-
- this.page.on('request', onRequest);
-
- await this.page.waitForResponse((response) => {
- return syncUrls.includes(response.request().url()) && response.status() === 200;
- });
-
- this.page.off('request', onRequest);
- }
-
public async waitNoSpinner(): Promise {
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
}
@@ -152,13 +134,6 @@ export class ElementSession {
return delay(ms);
}
- public async setOffline(enabled: boolean): Promise {
- const description = enabled ? "offline" : "back online";
- this.log.step(`goes ${description}`);
- await this.page.setOfflineMode(enabled);
- this.log.done();
- }
-
public async close(): Promise {
return this.browser.close();
}
diff --git a/test/end-to-end-tests/src/usecases/memberlist.ts b/test/end-to-end-tests/src/usecases/memberlist.ts
index 2aa61313a3..9daea8b1cb 100644
--- a/test/end-to-end-tests/src/usecases/memberlist.ts
+++ b/test/end-to-end-tests/src/usecases/memberlist.ts
@@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { strict as assert } from 'assert';
import { ElementHandle } from "puppeteer";
import { openRoomSummaryCard } from "./rightpanel";
@@ -29,46 +28,6 @@ export async function openMemberInfo(session: ElementSession, name: String): Pro
await matchingLabel.click();
}
-interface Device {
- id: string;
- key: string;
-}
-
-export async function verifyDeviceForUser(session: ElementSession, name: string,
- expectedDevice: Device): Promise {
- session.log.step(`verifies e2e device for ${name}`);
- const membersAndNames = await getMembersInMemberlist(session);
- const matchingLabel = membersAndNames.filter((m) => {
- return m.displayName === name;
- }).map((m) => m.label)[0];
- await matchingLabel.click();
- // click verify in member info
- const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify");
- await firstVerifyButton.click();
- // expect "Verify device" dialog and click "Begin Verification"
- const dialogHeader = await session.innerText(await session.query(".mx_Dialog .mx_Dialog_title"));
- assert(dialogHeader, "Verify device");
- const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary");
- await beginVerificationButton.click();
- // get emoji SAS labels
- const sasLabelElements = await session.queryAll(
- ".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label");
- const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e)));
- console.log("my sas labels", sasLabels);
-
- const dialogCodeFields = await session.queryAll(".mx_QuestionDialog code");
- assert.strictEqual(dialogCodeFields.length, 2);
- const deviceId = await session.innerText(dialogCodeFields[0]);
- const deviceKey = await session.innerText(dialogCodeFields[1]);
- assert.strictEqual(expectedDevice.id, deviceId);
- assert.strictEqual(expectedDevice.key, deviceKey);
- const confirmButton = await session.query(".mx_Dialog_primary");
- await confirmButton.click();
- const closeMemberInfo = await session.query(".mx_MemberInfo_cancel");
- await closeMemberInfo.click();
- session.log.done();
-}
-
interface MemberName {
label: ElementHandle;
displayName: string;
diff --git a/test/end-to-end-tests/src/usecases/timeline.ts b/test/end-to-end-tests/src/usecases/timeline.ts
index d12ccf9e19..ff88d63452 100644
--- a/test/end-to-end-tests/src/usecases/timeline.ts
+++ b/test/end-to-end-tests/src/usecases/timeline.ts
@@ -20,32 +20,6 @@ import { ElementHandle } from "puppeteer";
import { ElementSession } from "../session";
-export async function scrollToTimelineTop(session: ElementSession): Promise {
- session.log.step(`scrolls to the top of the timeline`);
- await session.page.evaluate(() => {
- return Promise.resolve().then(async () => {
- let timedOut = false;
- let timeoutHandle = null;
- // set scrollTop to 0 in a loop and check every 50ms
- // if content became available (scrollTop not being 0 anymore),
- // assume everything is loaded after 3s
- do {
- const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel");
- if (timelineScrollView && timelineScrollView.scrollTop !== 0) {
- if (timeoutHandle) {
- clearTimeout(timeoutHandle);
- }
- timeoutHandle = setTimeout(() => timedOut = true, 3000);
- timelineScrollView.scrollTop = 0;
- } else {
- await new Promise((resolve) => setTimeout(resolve, 50));
- }
- } while (!timedOut);
- });
- });
- session.log.done();
-}
-
interface Message {
sender: string;
encrypted?: boolean;
@@ -79,41 +53,6 @@ export async function receiveMessage(session: ElementSession, expectedMessage: M
session.log.done();
}
-export async function checkTimelineContains(session: ElementSession, expectedMessages: Message[],
- sendersDescription: string): Promise {
- session.log.step(`checks timeline contains ${expectedMessages.length} ` +
- `given messages${sendersDescription ? ` from ${sendersDescription}`:""}`);
- const eventTiles = await getAllEventTiles(session);
- let timelineMessages: Message[] = await Promise.all(eventTiles.map((eventTile) => {
- return getMessageFromEventTile(eventTile);
- }));
- //filter out tiles that were not messages
- timelineMessages = timelineMessages.filter((m) => !!m);
- timelineMessages.reduce((prevSender: string, m) => {
- if (m.continuation) {
- m.sender = prevSender;
- return prevSender;
- } else {
- return m.sender;
- }
- }, "");
-
- expectedMessages.forEach((expectedMessage) => {
- const foundMessage = timelineMessages.find((message) => {
- return message.sender === expectedMessage.sender &&
- message.body === expectedMessage.body;
- });
- try {
- assertMessage(foundMessage, expectedMessage);
- } catch (err) {
- console.log("timelineMessages", timelineMessages);
- throw err;
- }
- });
-
- session.log.done();
-}
-
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
assert.equal(foundMessage.body, expectedMessage.body);
@@ -127,10 +66,6 @@ function getLastEventTile(session: ElementSession): Promise {
return session.query(".mx_EventTile_last");
}
-function getAllEventTiles(session: ElementSession): Promise {
- return session.queryAll(".mx_RoomView_MessageList .mx_EventTile");
-}
-
async function getMessageFromEventTile(eventTile: ElementHandle): Promise {
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
const className: string = await (await eventTile.getProperty("className")).jsonValue();
diff --git a/test/end-to-end-tests/src/util.ts b/test/end-to-end-tests/src/util.ts
index 298e708007..130519f396 100644
--- a/test/end-to-end-tests/src/util.ts
+++ b/test/end-to-end-tests/src/util.ts
@@ -20,14 +20,6 @@ import { padEnd } from "lodash";
import { ElementSession } from "./session";
-export const range = function(start: number, amount: number, step = 1): Array {
- const r = [];
- for (let i = 0; i < amount; ++i) {
- r.push(start + (i * step));
- }
- return r;
-};
-
export const delay = function(ms: number): Promise {
return new Promise((resolve) => setTimeout(resolve, ms));
};
diff --git a/test/end-to-end-tests/start.ts b/test/end-to-end-tests/start.ts
index 4660ee01b0..378d24d2a0 100644
--- a/test/end-to-end-tests/start.ts
+++ b/test/end-to-end-tests/start.ts
@@ -19,7 +19,6 @@ import { Command } from "commander";
import { ElementSession } from './src/session';
import { scenario } from './src/scenario';
-import { RestSessionCreator } from './src/rest/creator';
const program = new Command();
@@ -54,12 +53,7 @@ async function runTests() {
options['executablePath'] = path;
}
- const restCreator = new RestSessionCreator(
- hsUrl,
- program.opts().registrationSharedSecret,
- );
-
- async function createSession(username) {
+ async function createSession(username: string) {
const session = await ElementSession.create(
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
);
@@ -69,7 +63,7 @@ async function runTests() {
let failure = false;
try {
- await scenario(createSession, restCreator);
+ await scenario(createSession);
} catch (err) {
failure = true;
console.log('failure: ', err);
diff --git a/yarn.lock b/yarn.lock
index cf119eca8a..67e9ab090d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3539,7 +3539,7 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
-cypress-real-events@^1.7.0:
+cypress-real-events@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ==