Convert end-to-end tests to Typescript (#7206)

This commit is contained in:
James Salter 2021-12-06 09:59:06 +11:00 committed by GitHub
parent 5219b6be80
commit d4813f7a1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 653 additions and 441 deletions

View file

@ -2,3 +2,4 @@ src/component-index.js
test/end-to-end-tests/node_modules/
test/end-to-end-tests/element/
test/end-to-end-tests/synapse/
test/end-to-end-tests/lib/

View file

@ -1,8 +1,67 @@
# Update on docker hub with the following commands in the directory of this file:
# docker build -t vectorim/element-web-ci-e2etests-env:latest .
# docker push vectorim/element-web-ci-e2etests-env:latest
# If you're on linux amd64
# docker build -t vectorim/element-web-ci-e2etests-env:latest .
# If you're on some other platform, you need to cross-compile
# docker buildx build --platform linux/amd64,linux/arm64 --push -t vectorim/element-web-ci-e2etests-env:latest .
# Then:
# docker push vectorim/element-web-ci-e2etests-env:latest
FROM node:14-buster
RUN apt-get update
RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
RUN apt-get -y install \
build-essential \
jq \
libffi-dev \
libjpeg-dev \
libssl-dev \
libxslt1-dev \
python3-dev \
python-pip \
python-setuptools \
python-virtualenv \
sqlite3 \
uuid-runtime
# dependencies for chrome (installed by puppeteer)
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm-dev libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
RUN apt-get -y install \
ca-certificates \
fonts-liberation \
gconf-service \
libappindicator1 \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm-dev \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils
RUN npm install -g typescript

View file

@ -67,7 +67,11 @@ const EncryptionInfo: React.FC<IProps> = ({
content = <PendingActionSpinner text={text} />;
} else {
content = (
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={onStartVerification}>
<AccessibleButton
kind="primary"
className="mx_UserInfo_wideButton mx_UserInfo_startVerification"
onClick={onStartVerification}
>
{ _t("Start Verification") }
</AccessibleButton>
);

View file

@ -2,3 +2,5 @@ node_modules
*.png
element/env
performance-entries.json
lib
logs

View file

@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc -p ./tsconfig.json"
},
"author": "",
"license": "ISC",
@ -15,5 +16,8 @@
"request": "^2.88.0",
"request-promise-native": "^1.0.7",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/puppeteer": "^5.4.4"
}
}

View file

@ -35,5 +35,6 @@ trap 'handle_error' ERR
if [ $has_custom_app -ne "1" ]; then
./element/start.sh
fi
node start.js $@
yarn build
node lib/start.js $@
stop_servers

View file

@ -0,0 +1,24 @@
/*
Copyright 2021 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-react-sdk/src/@types/global"; // load matrix-react-sdk's type extensions first
declare global {
interface Window {
mxPerformanceMonitor: any;
mxPerformanceEntryNames: any;
}
}

View file

@ -15,16 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports = class LogBuffer {
constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") {
import { Page, PageEventObject } from "puppeteer";
export class LogBuffer<EventMapperArg extends Parameters<Parameters<Page['on']>[1]>[0]> {
buffer: string;
constructor(
page: Page,
eventName: keyof PageEventObject,
eventMapper: (arg: EventMapperArg) => Promise<string>,
initialValue = "",
) {
this.buffer = initialValue;
page.on(eventName, (arg) => {
const result = eventMapper(arg);
if (reduceAsync) {
result.then((r) => this.buffer += r);
} else {
this.buffer += result;
}
page.on(eventName, (arg: EventMapperArg) => {
eventMapper(arg).then((r) => this.buffer += r);
});
}
};
}

View file

@ -15,14 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports = class Logger {
constructor(username) {
this.indent = 0;
this.username = username;
this.muted = false;
}
export class Logger {
private indent = 0;
private muted = false;
startGroup(description) {
constructor(readonly username: string) {}
public startGroup(description: string): Logger {
if (!this.muted) {
const indent = " ".repeat(this.indent * 2);
console.log(`${indent} * ${this.username} ${description}:`);
@ -31,12 +30,12 @@ module.exports = class Logger {
return this;
}
endGroup() {
public endGroup(): Logger {
this.indent -= 1;
return this;
}
step(description) {
public step(description: string): Logger {
if (!this.muted) {
const indent = " ".repeat(this.indent * 2);
process.stdout.write(`${indent} * ${this.username} ${description} ... `);
@ -44,20 +43,20 @@ module.exports = class Logger {
return this;
}
done(status = "done") {
public done(status = "done"): Logger {
if (!this.muted) {
process.stdout.write(status + "\n");
}
return this;
}
mute() {
public mute(): Logger {
this.muted = true;
return this;
}
unmute() {
public unmute(): Logger {
this.muted = false;
return this;
}
};
}

View file

@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const request = require('request-promise-native');
const cheerio = require('cheerio');
const url = require("url");
import request = require('request-promise-native');
import * as cheerio from 'cheerio';
import * as url from "url";
module.exports.approveConsent = async function(consentUrl) {
export const approveConsent = async function(consentUrl: string): Promise<void> {
const body = await request.get(consentUrl);
const doc = cheerio.load(body);
const v = doc("input[name=v]").val();

View file

@ -15,12 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { exec } = require('child_process');
const request = require('request-promise-native');
const RestSession = require('./session');
const RestMultiSession = require('./multi');
import { exec } from 'child_process';
import request = require('request-promise-native');
import { RestSession } from './session';
import { RestMultiSession } from './multi';
function execAsync(command, options) {
interface ExecResult {
stdout: string;
stderr: string;
}
function execAsync(command: string, options: Parameters<typeof exec>[1]): Promise<ExecResult> {
return new Promise((resolve, reject) => {
exec(command, options, (error, stdout, stderr) => {
if (error) {
@ -32,27 +37,32 @@ function execAsync(command, options) {
});
}
module.exports = class RestSessionCreator {
constructor(synapseSubdir, hsUrl, cwd) {
this.synapseSubdir = synapseSubdir;
this.hsUrl = hsUrl;
this.cwd = cwd;
}
export interface Credentials {
accessToken: string;
homeServer: string;
userId: string;
deviceId: string;
hsUrl: string;
}
async createSessionRange(usernames, password, groupName) {
export class RestSessionCreator {
constructor(private readonly synapseSubdir: string, private readonly hsUrl: string, private readonly cwd: string) {}
public async createSessionRange(usernames: string[], password: string,
groupName: string): Promise<RestMultiSession> {
const sessionPromises = usernames.map((username) => this.createSession(username, password));
const sessions = await Promise.all(sessionPromises);
return new RestMultiSession(sessions, groupName);
}
async createSession(username, password) {
await this._register(username, password);
public async createSession(username: string, password: string): Promise<RestSession> {
await this.register(username, password);
console.log(` * created REST user ${username} ... done`);
const authResult = await this._authenticate(username, password);
const authResult = await this.authenticate(username, password);
return new RestSession(authResult);
}
async _register(username, password) {
private async register(username: string, password: string): Promise<void> {
const registerArgs = [
'-c homeserver.yaml',
`-u ${username}`,
@ -70,7 +80,7 @@ module.exports = class RestSessionCreator {
await execAsync(allCmds, { cwd: this.cwd, encoding: 'utf-8' });
}
async _authenticate(username, password) {
private async authenticate(username: string, password: string): Promise<Credentials> {
const requestBody = {
"type": "m.login.password",
"identifier": {
@ -89,4 +99,4 @@ module.exports = class RestSessionCreator {
hsUrl: this.hsUrl,
};
}
};
}

View file

@ -15,19 +15,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const Logger = require('../logger');
import { Logger } from '../logger';
import { RestSession } from "./session";
import { RestRoom } from "./room";
module.exports = class RestMultiSession {
constructor(sessions, groupName) {
export class RestMultiSession {
readonly log: Logger;
constructor(public readonly sessions: RestSession[], groupName: string) {
this.log = new Logger(groupName);
this.sessions = sessions;
}
slice(groupName, start, end) {
public slice(groupName: string, start: number, end?: number): RestMultiSession {
return new RestMultiSession(this.sessions.slice(start, end), groupName);
}
pop(userName) {
public pop(userName: string): RestSession {
const idx = this.sessions.findIndex((s) => s.userName() === userName);
if (idx === -1) {
throw new Error(`user ${userName} not found`);
@ -36,9 +39,9 @@ module.exports = class RestMultiSession {
return session;
}
async setDisplayName(fn) {
public async setDisplayName(fn: (s: RestSession) => string): Promise<void> {
this.log.step("set their display name");
await Promise.all(this.sessions.map(async (s) => {
await Promise.all(this.sessions.map(async (s: RestSession) => {
s.log.mute();
await s.setDisplayName(fn(s));
s.log.unmute();
@ -46,7 +49,7 @@ module.exports = class RestMultiSession {
this.log.done();
}
async join(roomIdOrAlias) {
public async join(roomIdOrAlias: string): Promise<RestMultiRoom> {
this.log.step(`join ${roomIdOrAlias}`);
const rooms = await Promise.all(this.sessions.map(async (s) => {
s.log.mute();
@ -58,22 +61,19 @@ module.exports = class RestMultiSession {
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
}
room(roomIdOrAlias) {
public room(roomIdOrAlias: string): RestMultiRoom {
const rooms = this.sessions.map(s => s.room(roomIdOrAlias));
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
}
};
}
class RestMultiRoom {
constructor(rooms, roomIdOrAlias, log) {
this.rooms = rooms;
this.roomIdOrAlias = roomIdOrAlias;
this.log = log;
}
constructor(public readonly rooms: RestRoom[], private readonly roomIdOrAlias: string,
private readonly log: Logger) {}
async talk(message) {
public async talk(message: string): Promise<void> {
this.log.step(`say "${message}" in ${this.roomIdOrAlias}`);
await Promise.all(this.rooms.map(async (r) => {
await Promise.all(this.rooms.map(async (r: RestRoom) => {
r.log.mute();
await r.talk(message);
r.log.unmute();
@ -81,7 +81,7 @@ class RestMultiRoom {
this.log.done();
}
async leave() {
public async leave() {
this.log.step(`leave ${this.roomIdOrAlias}`);
await Promise.all(this.rooms.map(async (r) => {
r.log.mute();

View file

@ -15,20 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const uuidv4 = require('uuid/v4');
import uuidv4 = require('uuid/v4');
import { RestSession } from "./session";
import { Logger } from "../logger";
/* no pun intented */
module.exports = class RestRoom {
constructor(session, roomId, log) {
this.session = session;
this._roomId = roomId;
this.log = log;
}
export class RestRoom {
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
async talk(message) {
this.log.step(`says "${message}" in ${this._roomId}`);
async talk(message: string): Promise<void> {
this.log.step(`says "${message}" in ${this.roomId}`);
const txId = uuidv4();
await this.session._put(`/rooms/${this._roomId}/send/m.room.message/${txId}`, {
await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
"msgtype": "m.text",
"body": message,
});
@ -36,13 +34,9 @@ module.exports = class RestRoom {
return txId;
}
async leave() {
this.log.step(`leaves ${this._roomId}`);
await this.session._post(`/rooms/${this._roomId}/leave`);
async leave(): Promise<void> {
this.log.step(`leaves ${this.roomId}`);
await this.session.post(`/rooms/${this.roomId}/leave`);
this.log.done();
}
roomId() {
return this._roomId;
}
};
}

View file

@ -1,126 +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.
*/
const request = require('request-promise-native');
const Logger = require('../logger');
const RestRoom = require('./room');
const { approveConsent } = require('./consent');
module.exports = class RestSession {
constructor(credentials) {
this.log = new Logger(credentials.userId);
this._credentials = credentials;
this._displayName = null;
this._rooms = {};
}
userId() {
return this._credentials.userId;
}
userName() {
return this._credentials.userId.split(":")[0].substr(1);
}
displayName() {
return this._displayName;
}
async setDisplayName(displayName) {
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) {
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) {
if (this._rooms.hasOwnProperty(roomIdOrAlias)) {
return this._rooms[roomIdOrAlias];
} else {
throw new Error(`${this._credentials.userId} is not in ${roomIdOrAlias}`);
}
}
async createRoom(name, options) {
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, body) {
return this._request("POST", csApiPath, body);
}
_put(csApiPath, body) {
return this._request("PUT", csApiPath, body);
}
async _request(method, csApiPath, body) {
try {
const responseBody = await request({
url: `${this._credentials.hsUrl}/_matrix/client/r0${csApiPath}`,
method,
headers: {
"Authorization": `Bearer ${this._credentials.accessToken}`,
},
json: true,
body,
});
return responseBody;
} catch (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}`);
}
}
}
};

View file

@ -0,0 +1,137 @@
/*
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<string, RestRoom> = {};
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].substr(1);
}
displayName(): string {
return this._displayName;
}
async setDisplayName(displayName: string): Promise<void> {
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<RestRoom> {
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<RestRoom> {
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<any> {
return this.request("POST", csApiPath, body);
}
put(csApiPath: string, body?: any): Promise<any> {
return this.request("PUT", csApiPath, body);
}
async request(method: string, csApiPath: string, body?: any): Promise<any> {
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}`);
}
}
}
}

View file

@ -14,15 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { range } = require('./util');
const signup = require('./usecases/signup');
const toastScenarios = require('./scenarios/toast');
const roomDirectoryScenarios = require('./scenarios/directory');
const lazyLoadingScenarios = require('./scenarios/lazy-loading');
const e2eEncryptionScenarios = require('./scenarios/e2e-encryption');
const spacesScenarios = require('./scenarios/spaces');
import { range } from './util';
import { signup } from './usecases/signup';
import { toastScenarios } from './scenarios/toast';
import { roomDirectoryScenarios } from './scenarios/directory';
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 { spacesScenarios } from './scenarios/spaces';
import { RestSession } from "./rest/session";
module.exports = async function scenario(createSession, restCreator) {
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
restCreator: RestSessionCreator): Promise<void> {
let firstUser = true;
async function createUser(username) {
const session = await createSession(username);
@ -46,11 +51,11 @@ module.exports = async function scenario(createSession, restCreator) {
await lazyLoadingScenarios(alice, bob, charlies);
// do spaces scenarios last as the rest of the tests may get confused by spaces
await spacesScenarios(alice, bob);
};
}
async function createRestUsers(restCreator) {
async function createRestUsers(restCreator: RestSessionCreator): Promise<RestMultiSession> {
const usernames = range(1, 10).map((i) => `charly-${i}`);
const charlies = await restCreator.createSessionRange(usernames, "testtest", "charly-1..10");
await charlies.setDisplayName((s) => `Charly #${s.userName().split('-')[1]}`);
await charlies.setDisplayName((s: RestSession) => `Charly #${s.userName().split('-')[1]}`);
return charlies;
}

View file

@ -15,13 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const join = require('../usecases/join');
const sendMessage = require('../usecases/send-message');
const { receiveMessage } = require('../usecases/timeline');
const { createRoom } = require('../usecases/create-room');
const { changeRoomSettings } = require('../usecases/room-settings');
import { join } from '../usecases/join';
import { sendMessage } from '../usecases/send-message';
import { receiveMessage } from '../usecases/timeline';
import { createRoom } from '../usecases/create-room';
import { changeRoomSettings } from '../usecases/room-settings';
import { ElementSession } from "../session";
module.exports = async function roomDirectoryScenarios(alice, bob) {
export async function roomDirectoryScenarios(alice: ElementSession, bob: ElementSession) {
console.log(" creating a public room and join through directory:");
const room = 'test';
await createRoom(alice, room);
@ -33,4 +34,4 @@ module.exports = async function roomDirectoryScenarios(alice, bob) {
const aliceMessage = "hi Bob, welcome!";
await sendMessage(alice, aliceMessage);
await receiveMessage(bob, { sender: "alice", body: aliceMessage });
};
}

View file

@ -15,22 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const sendMessage = require('../usecases/send-message');
const acceptInvite = require('../usecases/accept-invite');
const { receiveMessage } = require('../usecases/timeline');
const { createDm } = require('../usecases/create-room');
const { checkRoomSettings } = require('../usecases/room-settings');
const { startSasVerification, acceptSasVerification } = require('../usecases/verify');
const { setupSecureBackup } = require('../usecases/security');
const assert = require('assert');
const { measureStart, measureStop } = require('../util');
import { ElementSession } from "../session";
module.exports = async function e2eEncryptionScenarios(alice, bob) {
import { sendMessage } from '../usecases/send-message';
import { acceptInvite } from '../usecases/accept-invite';
import { receiveMessage } from '../usecases/timeline';
import { createDm } from '../usecases/create-room';
import { checkRoomSettings } from '../usecases/room-settings';
import { startSasVerification, acceptSasVerification } from '../usecases/verify';
import { setupSecureBackup } from '../usecases/security';
import { strict as assert } from 'assert';
import { measureStart, measureStop } from '../util';
export async function e2eEncryptionScenarios(alice: ElementSession, bob: ElementSession) {
console.log(" creating an e2e encrypted DM and join through invite:");
await createDm(bob, ['@alice:localhost']);
await checkRoomSettings(bob, { encryption: true }); // for sanity, should be e2e-by-default
await acceptInvite(alice, 'bob');
// do sas verifcation
// do sas verification
bob.log.step(`starts SAS verification with ${alice.username}`);
await measureStart(bob, "mx_VerifyE2EEUser");
const bobSasPromise = startSasVerification(bob, alice.username);
@ -48,4 +50,4 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) {
await sendMessage(bob, bobMessage);
await receiveMessage(alice, { sender: "bob", body: bobMessage, encrypted: true });
await setupSecureBackup(alice);
};
}

View file

@ -15,24 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { delay } = require('../util');
const join = require('../usecases/join');
const sendMessage = require('../usecases/send-message');
const {
import { delay } from '../util';
import { join } from '../usecases/join';
import { sendMessage } from '../usecases/send-message';
import {
checkTimelineContains,
scrollToTimelineTop,
} = require('../usecases/timeline');
const { createRoom } = require('../usecases/create-room');
const { getMembersInMemberlist } = require('../usecases/memberlist');
const { changeRoomSettings } = require('../usecases/room-settings');
const assert = require('assert');
} from '../usecases/timeline';
import { createRoom } from '../usecases/create-room';
import { getMembersInMemberlist } from '../usecases/memberlist';
import { changeRoomSettings } from '../usecases/room-settings';
import { strict as assert } from 'assert';
import { RestMultiSession } from "../rest/multi";
import { ElementSession } from "../session";
module.exports = async function lazyLoadingScenarios(alice, bob, charlies) {
export async function lazyLoadingScenarios(alice: ElementSession,
bob: ElementSession, charlies: RestMultiSession): Promise<void> {
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);
assert(charly1to5.sessions.length == 5);
assert(charly6to10.sessions.length == 5);
await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5);
await checkPaginatedDisplayNames(alice, charly1to5);
await checkMemberList(alice, charly1to5);
@ -42,14 +45,15 @@ module.exports = async function lazyLoadingScenarios(alice, bob, charlies) {
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, bob, charlies) {
async function setupRoomWithBobAliceAndCharlies(alice: ElementSession, bob: ElementSession,
charlies: RestMultiSession): Promise<void> {
await createRoom(bob, room);
await changeRoomSettings(bob, { directory: true, visibility: "public", alias });
// wait for alias to be set by server after clicking "save"
@ -66,7 +70,7 @@ async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) {
await join(alice, alias);
}
async function checkPaginatedDisplayNames(alice, charlies) {
async function checkPaginatedDisplayNames(alice: ElementSession, charlies: RestMultiSession): Promise<void> {
await scrollToTimelineTop(alice);
//alice should see 2 messages from every charly with
//the correct display name
@ -81,7 +85,7 @@ async function checkPaginatedDisplayNames(alice, charlies) {
await checkTimelineContains(alice, expectedMessages, charlies.log.username);
}
async function checkMemberList(alice, charlies) {
async function checkMemberList(alice: ElementSession, charlies: RestMultiSession): Promise<void> {
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"));
@ -94,7 +98,7 @@ async function checkMemberList(alice, charlies) {
alice.log.done();
}
async function checkMemberListLacksCharlies(session, charlies) {
async function checkMemberListLacksCharlies(session: ElementSession, charlies: RestMultiSession): Promise<void> {
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) => {
@ -105,7 +109,7 @@ async function checkMemberListLacksCharlies(session, charlies) {
session.log.done();
}
async function joinCharliesWhileAliceIsOffline(alice, charly6to10) {
async function joinCharliesWhileAliceIsOffline(alice: ElementSession, charly6to10: RestMultiSession) {
await alice.setOffline(true);
await delay(1000);
const members6to10 = await charly6to10.join(alias);

View file

@ -1,3 +1,5 @@
import { ElementSession } from "../session";
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
@ -14,18 +16,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { createSpace, inviteSpace } = require("../usecases/create-space");
import { createSpace, inviteSpace } from "../usecases/create-space";
module.exports = async function spacesScenarios(alice, bob) {
export async function spacesScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
console.log(" creating a space for spaces scenarios:");
await alice.delay(1000); // wait for dialogs to close
await setupSpaceUsingAliceAndInviteBob(alice, bob);
};
}
const space = "Test Space";
async function setupSpaceUsingAliceAndInviteBob(alice, bob) {
async function setupSpaceUsingAliceAndInviteBob(alice: ElementSession, bob: ElementSession): Promise<void> {
await createSpace(alice, space);
await inviteSpace(alice, space, "@bob:localhost");
await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { assertNoToasts, acceptToast, rejectToast } = require("../usecases/toasts");
import { assertNoToasts, acceptToast, rejectToast } from "../usecases/toasts";
import { ElementSession } from "../session";
module.exports = async function toastScenarios(alice, bob) {
export async function toastScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
console.log(" checking and clearing toasts:");
alice.log.startGroup(`clears toasts`);
@ -48,4 +49,4 @@ module.exports = async function toastScenarios(alice, bob) {
await assertNoToasts(bob);
bob.log.done();
bob.log.endGroup();
};
}

View file

@ -15,30 +15,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const puppeteer = require('puppeteer');
const Logger = require('./logger');
const LogBuffer = require('./logbuffer');
const { delay } = require('./util');
import * as puppeteer from 'puppeteer';
import { Logger } from './logger';
import { LogBuffer } from './logbuffer';
import { delay } from './util';
const DEFAULT_TIMEOUT = 20000;
module.exports = class ElementSession {
constructor(browser, page, username, elementServer, hsUrl) {
this.browser = browser;
this.page = page;
this.hsUrl = hsUrl;
this.elementServer = elementServer;
this.username = username;
this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`);
this.networkLog = new LogBuffer(page, "requestfinished", async (req) => {
const type = req.resourceType();
const response = await req.response();
return `${type} ${response.status()} ${req.method()} ${req.url()} \n`;
}, true);
interface XHRLogger {
logs: () => string;
}
export class ElementSession {
readonly consoleLog: LogBuffer<puppeteer.ConsoleMessage>;
readonly networkLog: LogBuffer<puppeteer.HTTPRequest>;
readonly log: Logger;
constructor(readonly browser: puppeteer.Browser, readonly page: puppeteer.Page, readonly username: string,
readonly elementServer: string, readonly hsUrl: string) {
this.consoleLog = new LogBuffer(page, "console",
async (msg: puppeteer.ConsoleMessage) => Promise.resolve(`${msg.text()}\n`));
this.networkLog = new LogBuffer(page,
"requestfinished", async (req: puppeteer.HTTPRequest) => {
const type = req.resourceType();
const response = await req.response();
return `${type} ${response.status()} ${req.method()} ${req.url()} \n`;
});
this.log = new Logger(this.username);
}
static async create(username, puppeteerOptions, elementServer, hsUrl, throttleCpuFactor = 1) {
public static async create(username: string, puppeteerOptions: Parameters<typeof puppeteer.launch>[0],
elementServer: string, hsUrl: string, throttleCpuFactor = 1): Promise<ElementSession> {
const browser = await puppeteer.launch(puppeteerOptions);
const page = await browser.newPage();
await page.setViewport({
@ -53,7 +60,7 @@ module.exports = class ElementSession {
return new ElementSession(browser, page, username, elementServer, hsUrl);
}
async tryGetInnertext(selector) {
public async tryGetInnertext(selector: string): Promise<string> {
const field = await this.page.$(selector);
if (field != null) {
const textHandle = await field.getProperty('innerText');
@ -62,32 +69,32 @@ module.exports = class ElementSession {
return null;
}
async getElementProperty(handle, property) {
public async getElementProperty(handle: puppeteer.ElementHandle, property: string): Promise<string> {
const propHandle = await handle.getProperty(property);
return await propHandle.jsonValue();
}
innerText(field) {
public innerText(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'innerText');
}
getOuterHTML(field) {
public getOuterHTML(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'outerHTML');
}
isChecked(field) {
public isChecked(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'checked');
}
consoleLogs() {
public consoleLogs(): string {
return this.consoleLog.buffer;
}
networkLogs() {
public networkLogs(): string {
return this.networkLog.buffer;
}
logXHRRequests() {
public logXHRRequests(): XHRLogger {
let buffer = "";
this.page.on('requestfinished', async (req) => {
const type = req.resourceType();
@ -106,11 +113,11 @@ module.exports = class ElementSession {
};
}
async printElements(label, elements) {
public async printElements(label: string, elements: puppeteer.ElementHandle[] ): Promise<void> {
console.log(label, await Promise.all(elements.map(this.getOuterHTML)));
}
async replaceInputText(input, text) {
public async replaceInputText(input: puppeteer.ElementHandle, text: string): Promise<void> {
// click 3 times to select all text
await input.click({ clickCount: 3 });
// waiting here solves not having selected all the text by the 3x click above,
@ -122,21 +129,22 @@ module.exports = class ElementSession {
await input.type(text);
}
query(selector, timeout = DEFAULT_TIMEOUT, hidden = false) {
public query(selector: string, timeout: number = DEFAULT_TIMEOUT,
hidden = false): Promise<puppeteer.ElementHandle> {
return this.page.waitForSelector(selector, { visible: true, timeout, hidden });
}
async queryAll(selector) {
public async queryAll(selector: string): Promise<puppeteer.ElementHandle[]> {
const timeout = DEFAULT_TIMEOUT;
await this.query(selector, timeout);
return await this.page.$$(selector);
}
waitForReload() {
public waitForReload(): Promise<void> {
const timeout = DEFAULT_TIMEOUT;
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
this.browser.removeEventListener('domcontentloaded', callback);
this.page.off('domcontentloaded', callback);
reject(new Error(`timeout of ${timeout}ms for waitForReload elapsed`));
}, timeout);
@ -149,11 +157,11 @@ module.exports = class ElementSession {
});
}
waitForNewPage() {
public waitForNewPage(): Promise<void> {
const timeout = DEFAULT_TIMEOUT;
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
this.browser.removeListener('targetcreated', callback);
this.browser.off('targetcreated', callback);
reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`));
}, timeout);
@ -161,7 +169,7 @@ module.exports = class ElementSession {
if (target.type() !== 'page') {
return;
}
this.browser.removeListener('targetcreated', callback);
this.browser.off('targetcreated', callback);
clearTimeout(timeoutHandle);
const page = await target.page();
resolve(page);
@ -172,7 +180,7 @@ module.exports = class ElementSession {
}
/** wait for a /sync request started after this call that gets a 200 response */
async waitForNextSuccessfulSync() {
public async waitForNextSuccessfulSync(): Promise<void> {
const syncUrls = [];
function onRequest(request) {
if (request.url().indexOf("/sync") !== -1) {
@ -186,33 +194,33 @@ module.exports = class ElementSession {
return syncUrls.includes(response.request().url()) && response.status() === 200;
});
this.page.removeListener('request', onRequest);
this.page.off('request', onRequest);
}
goto(url) {
public goto(url: string): Promise<puppeteer.HTTPResponse> {
return this.page.goto(url);
}
url(path) {
public url(path: string): string {
return this.elementServer + path;
}
delay(ms) {
public delay(ms: number) {
return delay(ms);
}
async setOffline(enabled) {
public async setOffline(enabled: boolean): Promise<void> {
const description = enabled ? "offline" : "back online";
this.log.step(`goes ${description}`);
await this.page.setOfflineMode(enabled);
this.log.done();
}
async close() {
public async close(): Promise<void> {
return this.browser.close();
}
async poll(callback, interval = 100) {
public async poll(callback: () => Promise<boolean>, interval = 100): Promise<boolean> {
const timeout = DEFAULT_TIMEOUT;
let waited = 0;
while (waited < timeout) {
@ -224,4 +232,4 @@ module.exports = class ElementSession {
}
return false;
}
};
}

View file

@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { findSublist } = require("./create-room");
import { findSublist } from "./create-room";
import { ElementSession } from "../session";
module.exports = async function acceptInvite(session, name) {
export async function acceptInvite(session: ElementSession, name: string): Promise<void> {
session.log.step(`accepts "${name}" invite`);
const inviteSublist = await findSublist(session, "invites");
const invitesHandles = await inviteSublist.$$(".mx_RoomTile_name");
@ -35,4 +36,4 @@ module.exports = async function acceptInvite(session, name) {
await acceptInvitationLink.click();
session.log.done();
};
}

View file

@ -15,18 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { measureStart, measureStop } = require('../util');
import { measureStart, measureStop } from '../util';
import { ElementSession } from "../session";
import * as puppeteer from "puppeteer";
async function openRoomDirectory(session) {
export async function openRoomDirectory(session: ElementSession): Promise<void> {
const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
await roomDirectoryButton.click();
}
async function findSublist(session, name) {
export async function findSublist(session: ElementSession, name: string): Promise<puppeteer.ElementHandle> {
return await session.query(`.mx_RoomSublist[aria-label="${name}" i]`);
}
async function createRoom(session, roomName, encrypted=false) {
export async function createRoom(session: ElementSession, roomName: string, encrypted = false): Promise<void> {
session.log.step(`creates room "${roomName}"`);
const roomsSublist = await findSublist(session, "rooms");
@ -51,7 +53,7 @@ async function createRoom(session, roomName, encrypted=false) {
session.log.done();
}
async function createDm(session, invitees) {
export async function createDm(session: ElementSession, invitees: string[]): Promise<void> {
session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
await measureStart(session, "mx_CreateDM");
@ -83,5 +85,3 @@ async function createDm(session, invitees) {
await measureStop(session, "mx_CreateDM");
}
module.exports = { openRoomDirectory, findSublist, createRoom, createDm };

View file

@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
async function openSpaceCreateMenu(session) {
import { ElementSession } from "../session";
export async function openSpaceCreateMenu(session: ElementSession): Promise<void> {
const spaceCreateButton = await session.query('.mx_SpaceButton_new');
await spaceCreateButton.click();
}
async function createSpace(session, name, isPublic = false) {
export async function createSpace(session: ElementSession, name: string, isPublic = false): Promise<void> {
session.log.step(`creates space "${name}"`);
await openSpaceCreateMenu(session);
@ -50,7 +52,7 @@ async function createSpace(session, name, isPublic = false) {
session.log.done();
}
async function inviteSpace(session, spaceName, userId) {
export async function inviteSpace(session: ElementSession, spaceName: string, userId: string): Promise<void> {
session.log.step(`invites "${userId}" to space "${spaceName}"`);
const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`);
@ -76,5 +78,3 @@ async function inviteSpace(session, spaceName, userId) {
await confirmButton.click();
session.log.done();
}
module.exports = { openSpaceCreateMenu, createSpace, inviteSpace };

View file

@ -15,22 +15,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
async function assertDialog(session, expectedTitle) {
export async function assertDialog(session: ElementSession, expectedTitle: string): Promise<void> {
const titleElement = await session.query(".mx_Dialog .mx_Dialog_title");
const dialogHeader = await session.innerText(titleElement);
assert.equal(dialogHeader, expectedTitle);
}
async function acceptDialog(session, expectedTitle) {
export async function acceptDialog(session: ElementSession, expectedTitle: string): Promise<void> {
const foundDialog = await acceptDialogMaybe(session, expectedTitle);
if (!foundDialog) {
throw new Error("could not find a dialog");
}
}
async function acceptDialogMaybe(session, expectedTitle) {
export async function acceptDialogMaybe(session: ElementSession, expectedTitle: string): Promise<boolean> {
let primaryButton = null;
try {
primaryButton = await session.query(".mx_Dialog .mx_Dialog_primary");
@ -43,9 +44,3 @@ async function acceptDialogMaybe(session, expectedTitle) {
await primaryButton.click();
return true;
}
module.exports = {
assertDialog,
acceptDialog,
acceptDialogMaybe,
};

View file

@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports = async function invite(session, userId) {
import { ElementSession } from "../session";
export async function invite(session: ElementSession, userId: string): Promise<void> {
session.log.step(`invites "${userId}" to room`);
await session.delay(1000);
const memberPanelButton = await session.query(".mx_RightPanel_membersButton");
@ -38,4 +40,4 @@ module.exports = async function invite(session, userId) {
const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click();
session.log.done();
};
}

View file

@ -15,10 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { openRoomDirectory } = require('./create-room');
const { measureStart, measureStop } = require('../util');
import { openRoomDirectory } from './create-room';
import { measureStart, measureStop } from '../util';
import { ElementSession } from "../session";
module.exports = async function join(session, roomName) {
export async function join(session: ElementSession, roomName: string): Promise<void> {
session.log.step(`joins room "${roomName}"`);
await measureStart(session, "mx_JoinRoom");
await openRoomDirectory(session);
@ -30,4 +31,4 @@ module.exports = async function join(session, roomName) {
await session.query('.mx_MessageComposer');
await measureStop(session, "mx_JoinRoom");
session.log.done();
};
}

View file

@ -15,10 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
const { openRoomSummaryCard } = require("./rightpanel");
import { strict as assert } from 'assert';
import { openRoomSummaryCard } from "./rightpanel";
import { ElementSession } from "../session";
import { ElementHandle } from "puppeteer";
async function openMemberInfo(session, name) {
export async function openMemberInfo(session: ElementSession, name: String): Promise<void> {
const membersAndNames = await getMembersInMemberlist(session);
const matchingLabel = membersAndNames.filter((m) => {
return m.displayName === name;
@ -26,9 +28,13 @@ async function openMemberInfo(session, name) {
await matchingLabel.click();
}
module.exports.openMemberInfo = openMemberInfo;
interface Device {
id: string;
key: string;
}
module.exports.verifyDeviceForUser = async function(session, name, expectedDevice) {
export async function verifyDeviceForUser(session: ElementSession, name: string,
expectedDevice: Device): Promise<void> {
session.log.step(`verifies e2e device for ${name}`);
const membersAndNames = await getMembersInMemberlist(session);
const matchingLabel = membersAndNames.filter((m) => {
@ -50,19 +56,24 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic
console.log("my sas labels", sasLabels);
const dialogCodeFields = await session.queryAll(".mx_QuestionDialog code");
assert.equal(dialogCodeFields.length, 2);
assert.strictEqual(dialogCodeFields.length, 2);
const deviceId = await session.innerText(dialogCodeFields[0]);
const deviceKey = await session.innerText(dialogCodeFields[1]);
assert.equal(expectedDevice.id, deviceId);
assert.equal(expectedDevice.key, deviceKey);
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();
};
}
async function getMembersInMemberlist(session) {
interface MemberName {
label: ElementHandle;
displayName: string;
}
export async function getMembersInMemberlist(session: ElementSession): Promise<MemberName[]> {
await openRoomSummaryCard(session);
const memberPanelButton = await session.query(".mx_RoomSummaryCard_icon_people");
// We are back at the room summary card
@ -73,5 +84,3 @@ async function getMembersInMemberlist(session) {
return { label: el, displayName: await session.innerText(el) };
}));
}
module.exports.getMembersInMemberlist = getMembersInMemberlist;

View file

@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports.openRoomRightPanel = async function(session) {
import { ElementSession } from "../session";
export async function openRoomRightPanel(session: ElementSession): Promise<void> {
try {
await session.query('.mx_RoomHeader .mx_RightPanel_headerButton_highlight[aria-label="Room Info"]');
} catch (e) {
@ -22,9 +24,9 @@ module.exports.openRoomRightPanel = async function(session) {
const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');
await roomSummaryButton.click();
}
};
}
module.exports.goBackToRoomSummaryCard = async function(session) {
export async function goBackToRoomSummaryCard(session: ElementSession): Promise<void> {
for (let i = 0; i < 5; i++) {
try {
const backButton = await session.query(".mx_BaseCard_back", 500);
@ -39,9 +41,9 @@ module.exports.goBackToRoomSummaryCard = async function(session) {
}
}
}
};
}
module.exports.openRoomSummaryCard = async function(session) {
await module.exports.openRoomRightPanel(session);
await module.exports.goBackToRoomSummaryCard(session);
};
export async function openRoomSummaryCard(session: ElementSession) {
await openRoomRightPanel(session);
await goBackToRoomSummaryCard(session);
}

View file

@ -15,11 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
const { openRoomSummaryCard } = require("./rightpanel");
const { acceptDialog } = require('./dialog');
import { strict as assert } from 'assert';
import { openRoomSummaryCard } from "./rightpanel";
import { acceptDialog } from './dialog';
import { ElementSession } from "../session";
import { ElementHandle } from "puppeteer";
async function setSettingsToggle(session, toggle, enabled) {
export async function setSettingsToggle(session: ElementSession, toggle: ElementHandle, enabled): Promise<boolean> {
const className = await session.getElementProperty(toggle, "className");
const checked = className.includes("mx_ToggleSwitch_on");
if (checked !== enabled) {
@ -31,7 +33,8 @@ async function setSettingsToggle(session, toggle, enabled) {
}
}
async function checkSettingsToggle(session, toggle, shouldBeEnabled) {
export async function checkSettingsToggle(session: ElementSession,
toggle: ElementHandle, shouldBeEnabled: boolean): Promise<void> {
const className = await session.getElementProperty(toggle, "className");
const checked = className.includes("mx_ToggleSwitch_on");
if (checked === shouldBeEnabled) {
@ -42,7 +45,11 @@ async function checkSettingsToggle(session, toggle, shouldBeEnabled) {
}
}
async function findTabs(session) {
interface Tabs {
securityTabButton: ElementHandle;
}
async function findTabs(session: ElementSession): Promise<Tabs> {
/// XXX delay is needed here, possibly because the header is being rerendered
/// click doesn't do anything otherwise
await session.delay(1000);
@ -60,7 +67,14 @@ async function findTabs(session) {
return { securityTabButton };
}
async function checkRoomSettings(session, expectedSettings) {
interface Settings {
encryption: boolean;
directory?: boolean;
alias?: string;
visibility?: string;
}
export async function checkRoomSettings(session: ElementSession, expectedSettings: Settings): Promise<void> {
session.log.startGroup(`checks the room settings`);
const { securityTabButton } = await findTabs(session);
@ -76,7 +90,7 @@ async function checkRoomSettings(session, expectedSettings) {
session.log.step(`checks for local alias of ${expectedSettings.alias}`);
const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
await summary.click();
const localAliases = await session.query('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item');
const localAliases = await session.queryAll('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item');
const localAliasTexts = await Promise.all(localAliases.map(a => session.innerText(a)));
if (localAliasTexts.find(a => a.includes(expectedSettings.alias))) {
session.log.done("present");
@ -85,7 +99,7 @@ async function checkRoomSettings(session, expectedSettings) {
}
}
securityTabButton.click();
await securityTabButton.click();
await session.delay(500);
const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
const e2eEncryptionToggle = securitySwitches[0];
@ -122,7 +136,7 @@ async function checkRoomSettings(session, expectedSettings) {
session.log.endGroup();
}
async function changeRoomSettings(session, settings) {
export async function changeRoomSettings(session, settings) {
session.log.startGroup(`changes the room settings`);
const { securityTabButton } = await findTabs(session);
@ -179,5 +193,3 @@ async function changeRoomSettings(session, settings) {
session.log.endGroup();
}
module.exports = { checkRoomSettings, changeRoomSettings };

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { acceptToast } = require("./toasts");
import { acceptToast } from "./toasts";
import { ElementSession } from "../session";
async function setupSecureBackup(session) {
export async function setupSecureBackup(session: ElementSession): Promise<void> {
session.log.step("sets up Secure Backup");
await acceptToast(session, "Set up Secure Backup");

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
module.exports = async function sendMessage(session, message) {
export async function sendMessage(session: ElementSession, message: string): Promise<void> {
session.log.step(`writes "${message}" in room`);
// this selector needs to be the element that has contenteditable=true,
// not any if its parents, otherwise it behaves flaky at best.
@ -31,4 +32,4 @@ module.exports = async function sendMessage(session, message) {
// wait for the message to appear sent
await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)");
session.log.done();
};
}

View file

@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
async function openSettings(session, section) {
export async function openSettings(session: ElementSession, section: string): Promise<void> {
const menuButton = await session.query(".mx_UserMenu");
await menuButton.click();
const settingsItem = await session.query(".mx_UserMenu_iconSettings");
@ -29,7 +30,7 @@ async function openSettings(session, section) {
}
}
module.exports.enableLazyLoading = async function(session) {
export async function enableLazyLoading(session: ElementSession): Promise<void> {
session.log.step(`enables lazy loading of members in the lab settings`);
const settingsButton = await session.query('.mx_BottomLeftMenu_settings');
await settingsButton.click();
@ -39,17 +40,22 @@ module.exports.enableLazyLoading = async function(session) {
const closeButton = await session.query(".mx_RoomHeader_cancelButton");
await closeButton.click();
session.log.done();
};
}
module.exports.getE2EDeviceFromSettings = async function(session) {
interface E2EDevice {
id: string;
key: string;
}
export async function getE2EDeviceFromSettings(session: ElementSession): Promise<E2EDevice> {
session.log.step(`gets e2e device/key from settings`);
await openSettings(session, "security");
const deviceAndKey = await session.queryAll(".mx_SettingsTab_section .mx_CryptographyPanel code");
assert.equal(deviceAndKey.length, 2);
const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue();
const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue();
const id: string = await (await deviceAndKey[0].getProperty("innerText")).jsonValue();
const key: string = await (await deviceAndKey[1].getProperty("innerText")).jsonValue();
const closeButton = await session.query(".mx_UserSettingsDialog .mx_Dialog_cancelButton");
await closeButton.click();
session.log.done();
return { id, key };
};
}

View file

@ -15,9 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
module.exports = async function signup(session, username, password, homeserver) {
export async function signup(session: ElementSession, username: string, password: string,
homeserver: string): Promise<void> {
session.log.step("signs up");
await session.goto(session.url('/#/register'));
// change the homeserver by clicking the advanced section
@ -79,4 +81,4 @@ module.exports = async function signup(session, username, password, homeserver)
});
assert(foundHomeUrl);
session.log.done();
};
}

View file

@ -15,9 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
import { ElementHandle } from "puppeteer";
module.exports.scrollToTimelineTop = async function(session) {
export async function scrollToTimelineTop(session: ElementSession): Promise<void> {
session.log.step(`scrolls to the top of the timeline`);
await session.page.evaluate(() => {
return Promise.resolve().then(async () => {
@ -41,14 +43,21 @@ module.exports.scrollToTimelineTop = async function(session) {
});
});
session.log.done();
};
}
module.exports.receiveMessage = async function(session, expectedMessage) {
interface Message {
sender: string;
encrypted?: boolean;
body: string;
continuation?: boolean;
}
export async function receiveMessage(session: ElementSession, expectedMessage: Message): Promise<void> {
session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`);
// wait for a response to come in that contains the message
// crude, but effective
async function getLastMessage() {
async function getLastMessage(): Promise<Message> {
const lastTile = await getLastEventTile(session);
return getMessageFromEventTile(lastTile);
}
@ -67,25 +76,26 @@ module.exports.receiveMessage = async function(session, expectedMessage) {
});
assertMessage(lastMessage, expectedMessage);
session.log.done();
};
}
module.exports.checkTimelineContains = async function(session, expectedMessages, sendersDescription) {
export async function checkTimelineContains(session: ElementSession, expectedMessages: Message[],
sendersDescription: string): Promise<void> {
session.log.step(`checks timeline contains ${expectedMessages.length} ` +
`given messages${sendersDescription ? ` from ${sendersDescription}`:""}`);
const eventTiles = await getAllEventTiles(session);
let timelineMessages = await Promise.all(eventTiles.map((eventTile) => {
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, 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) => {
@ -101,9 +111,9 @@ module.exports.checkTimelineContains = async function(session, expectedMessages,
});
session.log.done();
};
}
function assertMessage(foundMessage, expectedMessage) {
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
assert.equal(foundMessage.body, expectedMessage.body);
assert.equal(foundMessage.sender, expectedMessage.sender);
@ -112,17 +122,17 @@ function assertMessage(foundMessage, expectedMessage) {
}
}
function getLastEventTile(session) {
function getLastEventTile(session: ElementSession): Promise<ElementHandle> {
return session.query(".mx_EventTile_last");
}
function getAllEventTiles(session) {
function getAllEventTiles(session: ElementSession): Promise<ElementHandle[]> {
return session.queryAll(".mx_RoomView_MessageList .mx_EventTile");
}
async function getMessageFromEventTile(eventTile) {
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
const senderElement = await eventTile.$(".mx_SenderProfile_displayName");
const className = await (await eventTile.getProperty("className")).jsonValue();
const className: string = await (await eventTile.getProperty("className")).jsonValue();
const classNames = className.split(" ");
const bodyElement = await eventTile.$(".mx_EventTile_body");
let sender = null;
@ -132,7 +142,7 @@ async function getMessageFromEventTile(eventTile) {
if (!bodyElement) {
return null;
}
const body = await(await bodyElement.getProperty("innerText")).jsonValue();
const body: string = await(await bodyElement.getProperty("innerText")).jsonValue();
return {
sender,

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
async function assertNoToasts(session) {
export async function assertNoToasts(session: ElementSession): Promise<void> {
try {
await session.query('.mx_Toast_toast', 1000, true);
} catch (e) {
@ -26,22 +27,20 @@ async function assertNoToasts(session) {
}
}
async function assertToast(session, expectedTitle) {
export async function assertToast(session: ElementSession, expectedTitle: string): Promise<void> {
const h2Element = await session.query('.mx_Toast_title h2');
const toastTitle = await session.innerText(h2Element);
assert.equal(toastTitle, expectedTitle);
}
async function acceptToast(session, expectedTitle) {
export async function acceptToast(session: ElementSession, expectedTitle: string): Promise<void> {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_primary');
await btn.click();
}
async function rejectToast(session, expectedTitle) {
export async function rejectToast(session: ElementSession, expectedTitle: string): Promise<void> {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline');
await btn.click();
}
module.exports = { assertNoToasts, assertToast, acceptToast, rejectToast };

View file

@ -15,10 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
const { openMemberInfo } = require("./memberlist");
import { strict as assert } from 'assert';
import { openMemberInfo } from "./memberlist";
import { ElementSession } from "../session";
async function startVerification(session, name) {
export async function startVerification(session: ElementSession, name: string): Promise<void> {
session.log.step("opens their opponent's profile and starts verification");
await openMemberInfo(session, name);
// click verify in member info
@ -29,22 +30,22 @@ async function startVerification(session, name) {
await session.delay(1000);
// click 'start verification'
const startVerifyButton = await session.query('.mx_UserInfo_container .mx_AccessibleButton_kind_primary');
const startVerifyButton = await session.query('.mx_UserInfo_container .mx_UserInfo_startVerification');
await startVerifyButton.click();
session.log.done();
}
async function getSasCodes(session) {
async function getSasCodes(session: ElementSession): Promise<string[]> {
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)));
return sasLabels;
}
async function doSasVerification(session) {
async function doSasVerification(session: ElementSession): Promise<string[]> {
session.log.step("hunts for the emoji to yell at their opponent");
const sasCodes = await getSasCodes(session);
session.log.done(sasCodes);
session.log.done(sasCodes.join("\n"));
// Assume they match
session.log.step("assumes the emoji match");
@ -74,7 +75,7 @@ async function doSasVerification(session) {
return sasCodes;
}
module.exports.startSasVerification = async function(session, name) {
export async function startSasVerification(session: ElementSession, name: string): Promise<string[]> {
session.log.startGroup("starts verification");
await startVerification(session, name);
@ -84,9 +85,9 @@ module.exports.startSasVerification = async function(session, name) {
const sasCodes = await doSasVerification(session);
session.log.endGroup();
return sasCodes;
};
}
module.exports.acceptSasVerification = async function(session, name) {
export async function acceptSasVerification(session: ElementSession, name: string): Promise<string[]> {
session.log.startGroup("accepts verification");
const requestToast = await session.query('.mx_Toast_icon_verification');
@ -110,4 +111,4 @@ module.exports.acceptSasVerification = async function(session, name) {
const sasCodes = await doSasVerification(session);
session.log.endGroup();
return sasCodes;
};
}

View file

@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports.range = function(start, amount, step = 1) {
import { ElementSession } from "./session";
export const range = function(start: number, amount: number, step = 1): Array<number> {
const r = [];
for (let i = 0; i < amount; ++i) {
r.push(start + (i * step));
@ -23,17 +25,17 @@ module.exports.range = function(start, amount, step = 1) {
return r;
};
module.exports.delay = function(ms) {
export const delay = function(ms): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
};
module.exports.measureStart = function(session, name) {
export const measureStart = function(session: ElementSession, name: string): Promise<void> {
return session.page.evaluate(_name => {
window.mxPerformanceMonitor.start(_name);
}, name);
};
module.exports.measureStop = function(session, name) {
export const measureStop = function(session: ElementSession, name: string): Promise<void> {
return session.page.evaluate(_name => {
window.mxPerformanceMonitor.stop(_name);
}, name);

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const ElementSession = require('./src/session');
const scenario = require('./src/scenario');
const RestSessionCreator = require('./src/rest/creator');
const fs = require("fs");
import { ElementSession } from './src/session';
import { scenario } from './src/scenario';
import { RestSessionCreator } from './src/rest/creator';
import * as fs from "fs";
const program = require('commander');
import program = require('commander');
program
.option('--no-logs', "don't output logs, document html on error", false)
.option('--app-url [url]', "url to test", "http://localhost:5000")
@ -47,11 +47,11 @@ async function runTests() {
if (process.env.CHROME_PATH) {
const path = process.env.CHROME_PATH;
console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`);
options.executablePath = path;
options['executablePath'] = path;
}
const restCreator = new RestSessionCreator(
'synapse/installations/consent/env/bin',
'../synapse/installations/consent/env/bin',
hsUrl,
__dirname,
);
@ -84,7 +84,7 @@ async function runTests() {
await Promise.all(sessions.map(async (session) => {
// Collecting all performance monitoring data before closing the session
const measurements = await session.page.evaluate(() => {
let measurements = [];
let measurements;
window.mxPerformanceMonitor.addPerformanceDataCallback({
entryNames: [
window.mxPerformanceEntryNames.REGISTER,
@ -106,7 +106,9 @@ async function runTests() {
performanceEntries = JSON.parse(measurements);
return session.close();
}));
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));
if (performanceEntries) {
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));
}
if (failure) {
process.exit(-1);
} else {

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": false,
"outDir": "./lib",
"declaration": true,
"lib": [
"es2019",
"dom",
"dom.iterable"
],
},
"include": [
"./src/**/*.ts",
"start.ts"
]
}

View file

@ -7,6 +7,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.1.tgz#d90123f6c61fdf2f7cddd286ddae891586dd3488"
integrity sha512-sKDlqv6COJrR7ar0+GqqhrXQDzQlMcqMnF2iEU6m9hLo8kxozoAGUazwPyELHlRVmjsbvlnGXjnzyptSXVmceA==
"@types/puppeteer@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.4.tgz#e92abeccc4f46207c3e1b38934a1246be080ccd0"
integrity sha512-3Nau+qi69CN55VwZb0ATtdUAlYlqOOQ3OfQfq0Hqgc4JMFXiQT/XInlwQ9g6LbicDslE6loIFsXFklGh5XmI6Q==
dependencies:
"@types/node" "*"
"@types/yauzl@^2.9.1":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"