Move Lazy Loading tests from Puppeteer to Cypress (#8982)
* Remove Puppeteer Lazy Loading tests * Remove Puppeteer Lazy Loading tests * Remove Puppeteer Lazy Loading tests * Stash lazy loading cypress tests * Stash lazy loading cypress tests * Update cypress-real-events * Stash offline-less test * Add offline/online'ing
This commit is contained in:
parent
77d8a242af
commit
42ff9d6dc8
20 changed files with 348 additions and 689 deletions
174
cypress/e2e/lazy-loading/lazy-loading.spec.ts
Normal file
174
cypress/e2e/lazy-loading/lazy-loading.spec.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<string>("@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<JQuery> {
|
||||||
|
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<string>("@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<string>("@roomId").then(async roomId => {
|
||||||
|
for (const charly of charlies) {
|
||||||
|
await charly.client.leave(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkMemberListLacksCharlies(charlies);
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
import request from "browser-request";
|
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 { SynapseInstance } from "../plugins/synapsedocker";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
@ -31,10 +31,15 @@ interface CreateBotOpts {
|
||||||
* The display name to give to that bot user
|
* The display name to give to that bot user
|
||||||
*/
|
*/
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
/**
|
||||||
|
* Whether or not to start the syncing client.
|
||||||
|
*/
|
||||||
|
startClient?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCreateBotOptions = {
|
const defaultCreateBotOptions = {
|
||||||
autoAcceptInvites: true,
|
autoAcceptInvites: true,
|
||||||
|
startClient: true,
|
||||||
} as CreateBotOpts;
|
} as CreateBotOpts;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -59,6 +64,13 @@ declare global {
|
||||||
* @param roomName Name of the room to join
|
* @param roomName Name of the room to join
|
||||||
*/
|
*/
|
||||||
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable<Room>;
|
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable<Room>;
|
||||||
|
/**
|
||||||
|
* 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<ISendEventResponse>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +100,10 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts.startClient) {
|
||||||
|
return cy.wrap(cli);
|
||||||
|
}
|
||||||
|
|
||||||
return cy.wrap(
|
return cy.wrap(
|
||||||
cli.initCrypto()
|
cli.initCrypto()
|
||||||
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
||||||
|
@ -114,3 +130,14 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
|
||||||
|
|
||||||
return cy.wrap(Promise.reject());
|
return cy.wrap(Promise.reject());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("botSendMessage", (
|
||||||
|
cli: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
message: string,
|
||||||
|
): Chainable<ISendEventResponse> => {
|
||||||
|
return cy.wrap(cli.sendMessage(roomId, {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: message,
|
||||||
|
}), { log: false });
|
||||||
|
});
|
||||||
|
|
|
@ -124,6 +124,11 @@ declare global {
|
||||||
* Boostraps cross-signing.
|
* Boostraps cross-signing.
|
||||||
*/
|
*/
|
||||||
bootstrapCrossSigning(): Chainable<void>;
|
bootstrapCrossSigning(): Chainable<void>;
|
||||||
|
/**
|
||||||
|
* Joins the given room by alias or ID
|
||||||
|
* @param roomIdOrAlias the id or alias of the room to join
|
||||||
|
*/
|
||||||
|
joinRoom(roomIdOrAlias: string): Chainable<Room>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,3 +222,7 @@ Cypress.Commands.add("bootstrapCrossSigning", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => {
|
||||||
|
return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
|
||||||
|
});
|
||||||
|
|
|
@ -33,3 +33,5 @@ import "./percy";
|
||||||
import "./webserver";
|
import "./webserver";
|
||||||
import "./views";
|
import "./views";
|
||||||
import "./iframes";
|
import "./iframes";
|
||||||
|
import "./timeline";
|
||||||
|
import "./network";
|
||||||
|
|
62
cypress/support/network.ts
Normal file
62
cypress/support/network.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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 { };
|
68
cypress/support/timeline.ts
Normal file
68
cypress/support/timeline.ts
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<JQuery>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<JQuery> => {
|
||||||
|
// 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 { };
|
|
@ -172,7 +172,7 @@
|
||||||
"blob-polyfill": "^6.0.20211015",
|
"blob-polyfill": "^6.0.20211015",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"cypress": "^10.3.0",
|
"cypress": "^10.3.0",
|
||||||
"cypress-real-events": "^1.7.0",
|
"cypress-real-events": "^1.7.1",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-to-json": "^3.6.2",
|
"enzyme-to-json": "^3.6.2",
|
||||||
"eslint": "8.9.0",
|
"eslint": "8.9.0",
|
||||||
|
|
|
@ -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<void> {
|
|
||||||
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 });
|
|
||||||
};
|
|
|
@ -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<RestMultiSession> {
|
|
||||||
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<RestSession> {
|
|
||||||
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<void> {
|
|
||||||
// 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<Credentials> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<void> {
|
|
||||||
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<RestMultiRoom> {
|
|
||||||
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<void> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<string> {
|
|
||||||
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<void> {
|
|
||||||
this.log.step(`leaves ${this.roomId}`);
|
|
||||||
await this.session.post(`/rooms/${this.roomId}/leave`);
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<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].slice(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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,18 +15,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { range } from './util';
|
|
||||||
import { signup } from './usecases/signup';
|
import { signup } from './usecases/signup';
|
||||||
import { toastScenarios } from './scenarios/toast';
|
import { toastScenarios } from './scenarios/toast';
|
||||||
import { lazyLoadingScenarios } from './scenarios/lazy-loading';
|
|
||||||
import { e2eEncryptionScenarios } from './scenarios/e2e-encryption';
|
import { e2eEncryptionScenarios } from './scenarios/e2e-encryption';
|
||||||
import { ElementSession } from "./session";
|
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<ElementSession>,
|
export async function scenario(createSession: (s: string) => Promise<ElementSession>): Promise<void> {
|
||||||
restCreator: RestSessionCreator): Promise<void> {
|
|
||||||
let firstUser = true;
|
let firstUser = true;
|
||||||
async function createUser(username: string) {
|
async function createUser(username: string) {
|
||||||
const session = await createSession(username);
|
const session = await createSession(username);
|
||||||
|
@ -45,14 +39,4 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
|
|
||||||
await toastScenarios(alice, bob);
|
await toastScenarios(alice, bob);
|
||||||
await e2eEncryptionScenarios(alice, bob);
|
await e2eEncryptionScenarios(alice, bob);
|
||||||
console.log("create REST users:");
|
|
||||||
const charlies = await createRestUsers(restCreator);
|
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
|
||||||
}
|
|
||||||
|
|
||||||
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: RestSession) => `Charly #${s.userName().split('-')[1]}`);
|
|
||||||
return charlies;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<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);
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<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"));
|
|
||||||
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<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) => {
|
|
||||||
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);
|
|
||||||
}
|
|
|
@ -118,24 +118,6 @@ export class ElementSession {
|
||||||
return await this.page.$$(selector);
|
return await this.page.$$(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** wait for a /sync request started after this call that gets a 200 response */
|
|
||||||
public async waitForNextSuccessfulSync(): Promise<void> {
|
|
||||||
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<void> {
|
public async waitNoSpinner(): Promise<void> {
|
||||||
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
|
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
|
||||||
}
|
}
|
||||||
|
@ -152,13 +134,6 @@ export class ElementSession {
|
||||||
return delay(ms);
|
return delay(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async close(): Promise<void> {
|
public async close(): Promise<void> {
|
||||||
return this.browser.close();
|
return this.browser.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { strict as assert } from 'assert';
|
|
||||||
import { ElementHandle } from "puppeteer";
|
import { ElementHandle } from "puppeteer";
|
||||||
|
|
||||||
import { openRoomSummaryCard } from "./rightpanel";
|
import { openRoomSummaryCard } from "./rightpanel";
|
||||||
|
@ -29,46 +28,6 @@ export async function openMemberInfo(session: ElementSession, name: String): Pro
|
||||||
await matchingLabel.click();
|
await matchingLabel.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Device {
|
|
||||||
id: string;
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
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 {
|
interface MemberName {
|
||||||
label: ElementHandle;
|
label: ElementHandle;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
|
@ -20,32 +20,6 @@ import { ElementHandle } from "puppeteer";
|
||||||
|
|
||||||
import { ElementSession } from "../session";
|
import { ElementSession } from "../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 () => {
|
|
||||||
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 {
|
interface Message {
|
||||||
sender: string;
|
sender: string;
|
||||||
encrypted?: boolean;
|
encrypted?: boolean;
|
||||||
|
@ -79,41 +53,6 @@ export async function receiveMessage(session: ElementSession, expectedMessage: M
|
||||||
session.log.done();
|
session.log.done();
|
||||||
}
|
}
|
||||||
|
|
||||||
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: 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 {
|
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
|
||||||
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
|
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
|
||||||
assert.equal(foundMessage.body, expectedMessage.body);
|
assert.equal(foundMessage.body, expectedMessage.body);
|
||||||
|
@ -127,10 +66,6 @@ function getLastEventTile(session: ElementSession): Promise<ElementHandle> {
|
||||||
return session.query(".mx_EventTile_last");
|
return session.query(".mx_EventTile_last");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllEventTiles(session: ElementSession): Promise<ElementHandle[]> {
|
|
||||||
return session.queryAll(".mx_RoomView_MessageList .mx_EventTile");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
|
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
|
||||||
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
|
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
|
||||||
const className: string = await (await eventTile.getProperty("className")).jsonValue();
|
const className: string = await (await eventTile.getProperty("className")).jsonValue();
|
||||||
|
|
|
@ -20,14 +20,6 @@ import { padEnd } from "lodash";
|
||||||
|
|
||||||
import { ElementSession } from "./session";
|
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));
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const delay = function(ms: number): Promise<void> {
|
export const delay = function(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { Command } from "commander";
|
||||||
|
|
||||||
import { ElementSession } from './src/session';
|
import { ElementSession } from './src/session';
|
||||||
import { scenario } from './src/scenario';
|
import { scenario } from './src/scenario';
|
||||||
import { RestSessionCreator } from './src/rest/creator';
|
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
@ -54,12 +53,7 @@ async function runTests() {
|
||||||
options['executablePath'] = path;
|
options['executablePath'] = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const restCreator = new RestSessionCreator(
|
async function createSession(username: string) {
|
||||||
hsUrl,
|
|
||||||
program.opts().registrationSharedSecret,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function createSession(username) {
|
|
||||||
const session = await ElementSession.create(
|
const session = await ElementSession.create(
|
||||||
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
|
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
|
||||||
);
|
);
|
||||||
|
@ -69,7 +63,7 @@ async function runTests() {
|
||||||
|
|
||||||
let failure = false;
|
let failure = false;
|
||||||
try {
|
try {
|
||||||
await scenario(createSession, restCreator);
|
await scenario(createSession);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failure = true;
|
failure = true;
|
||||||
console.log('failure: ', err);
|
console.log('failure: ', err);
|
||||||
|
|
|
@ -3539,7 +3539,7 @@ csstype@^3.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||||
|
|
||||||
cypress-real-events@^1.7.0:
|
cypress-real-events@^1.7.1:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
|
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
|
||||||
integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ==
|
integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ==
|
||||||
|
|
Loading…
Reference in a new issue