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==