mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Add basic performance testing via Cypress (#8586)
This commit is contained in:
parent
83b3dfa341
commit
c122c5cd3b
17 changed files with 169 additions and 18 deletions
14
.github/workflows/element-build-and-test.yaml
vendored
14
.github/workflows/element-build-and-test.yaml
vendored
|
@ -88,6 +88,20 @@ jobs:
|
||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/synapselogs
|
cypress/synapselogs
|
||||||
|
|
||||||
|
- name: Store benchmark result
|
||||||
|
if: github.ref == 'refs/heads/develop'
|
||||||
|
uses: matrix-org/github-action-benchmark@jsperfentry-1
|
||||||
|
with:
|
||||||
|
name: Cypress measurements
|
||||||
|
tool: 'jsperformanceentry'
|
||||||
|
output-file-path: cypress/performance/measurements.json
|
||||||
|
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/cypress/bench/
|
||||||
|
benchmark-data-dir-path: cypress/bench
|
||||||
|
fail-on-alert: false
|
||||||
|
comment-on-alert: false
|
||||||
|
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
|
||||||
|
auto-push: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
|
||||||
app-tests:
|
app-tests:
|
||||||
name: Element Web Integration Tests
|
name: Element Web Integration Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
14
.github/workflows/end-to-end-tests.yaml
vendored
14
.github/workflows/end-to-end-tests.yaml
vendored
|
@ -40,20 +40,18 @@ jobs:
|
||||||
test/end-to-end-tests/synapse/installations/consent/homeserver.log
|
test/end-to-end-tests/synapse/installations/consent/homeserver.log
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
- name: Download previous benchmark data
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ./cache
|
|
||||||
key: ${{ runner.os }}-benchmark
|
|
||||||
|
|
||||||
- name: Store benchmark result
|
- name: Store benchmark result
|
||||||
|
if: github.ref == 'refs/heads/develop'
|
||||||
uses: matrix-org/github-action-benchmark@jsperfentry-1
|
uses: matrix-org/github-action-benchmark@jsperfentry-1
|
||||||
with:
|
with:
|
||||||
tool: 'jsperformanceentry'
|
tool: 'jsperformanceentry'
|
||||||
output-file-path: test/end-to-end-tests/performance-entries.json
|
output-file-path: test/end-to-end-tests/performance-entries.json
|
||||||
|
# This is the default dashboard path. It's included here anyway to
|
||||||
|
# make the difference from the Cypress variant in
|
||||||
|
# `element-build-and-test.yaml` more obvious.
|
||||||
|
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/dev/bench/
|
||||||
|
benchmark-data-dir-path: dev/bench
|
||||||
fail-on-alert: false
|
fail-on-alert: false
|
||||||
comment-on-alert: false
|
comment-on-alert: false
|
||||||
# Only temporary to monitor where failures occur
|
|
||||||
alert-comment-cc-users: '@gsouquet'
|
|
||||||
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
|
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
|
||||||
auto-push: ${{ github.ref == 'refs/heads/develop' }}
|
auto-push: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -27,3 +27,4 @@ package-lock.json
|
||||||
# These could have files in them but don't currently
|
# These could have files in them but don't currently
|
||||||
# Cypress will still auto-create them though...
|
# Cypress will still auto-create them though...
|
||||||
/cypress/fixtures
|
/cypress/fixtures
|
||||||
|
/cypress/performance
|
||||||
|
|
8
cypress/global.d.ts
vendored
8
cypress/global.d.ts
vendored
|
@ -18,6 +18,7 @@ import "matrix-js-sdk/src/@types/global";
|
||||||
import type { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
import type { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import type { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
|
import type { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
|
||||||
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
|
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
|
||||||
|
import type PerformanceMonitor from "../src/performance";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
@ -27,6 +28,7 @@ declare global {
|
||||||
matrixClient?: MatrixClient;
|
matrixClient?: MatrixClient;
|
||||||
};
|
};
|
||||||
mxDispatcher: MatrixDispatcher;
|
mxDispatcher: MatrixDispatcher;
|
||||||
|
mxPerformanceMonitor: PerformanceMonitor;
|
||||||
beforeReload?: boolean; // for detecting reloads
|
beforeReload?: boolean; // for detecting reloads
|
||||||
// Partial type for the matrix-js-sdk module, exported by browser-matrix
|
// Partial type for the matrix-js-sdk module, exported by browser-matrix
|
||||||
matrixcs: {
|
matrixcs: {
|
||||||
|
@ -38,7 +40,11 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
mxDispatcher: MatrixDispatcher; // to appease the MatrixDispatcher import
|
// to appease the MatrixDispatcher import
|
||||||
|
mxDispatcher: MatrixDispatcher;
|
||||||
|
// to appease the PerformanceMonitor import
|
||||||
|
mxPerformanceMonitor: PerformanceMonitor;
|
||||||
|
mxPerformanceEntryNames: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,15 @@ describe("Registration", () => {
|
||||||
cy.get("#mx_RegistrationForm_username").type("alice");
|
cy.get("#mx_RegistrationForm_username").type("alice");
|
||||||
cy.get("#mx_RegistrationForm_password").type("totally a great password");
|
cy.get("#mx_RegistrationForm_password").type("totally a great password");
|
||||||
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
|
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
|
||||||
|
cy.startMeasuring("create-account");
|
||||||
cy.get(".mx_Login_submit").click();
|
cy.get(".mx_Login_submit").click();
|
||||||
|
|
||||||
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
|
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
|
||||||
|
cy.stopMeasuring("create-account");
|
||||||
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
|
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
|
||||||
|
cy.startMeasuring("from-submit-to-home");
|
||||||
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
||||||
cy.url().should('contain', '/#/home');
|
cy.url().should('contain', '/#/home');
|
||||||
|
cy.stopMeasuring("from-submit-to-home");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,9 +49,11 @@ describe("Login", () => {
|
||||||
|
|
||||||
cy.get("#mx_LoginForm_username").type(username);
|
cy.get("#mx_LoginForm_username").type(username);
|
||||||
cy.get("#mx_LoginForm_password").type(password);
|
cy.get("#mx_LoginForm_password").type(password);
|
||||||
|
cy.startMeasuring("from-submit-to-home");
|
||||||
cy.get(".mx_Login_submit").click();
|
cy.get(".mx_Login_submit").click();
|
||||||
|
|
||||||
cy.url().should('contain', '/#/home');
|
cy.url().should('contain', '/#/home');
|
||||||
|
cy.stopMeasuring("from-submit-to-home");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,10 +54,12 @@ describe("Create Room", () => {
|
||||||
// Fill room address
|
// Fill room address
|
||||||
cy.get('[label="Room address"]').type("test-room-1");
|
cy.get('[label="Room address"]').type("test-room-1");
|
||||||
// Submit
|
// Submit
|
||||||
|
cy.startMeasuring("from-submit-to-room");
|
||||||
cy.get(".mx_Dialog_primary").click();
|
cy.get(".mx_Dialog_primary").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.url().should("contain", "/#/room/#test-room-1:localhost");
|
cy.url().should("contain", "/#/room/#test-room-1:localhost");
|
||||||
|
cy.stopMeasuring("from-submit-to-room");
|
||||||
cy.get(".mx_RoomHeader_nametext").contains(name);
|
cy.get(".mx_RoomHeader_nametext").contains(name);
|
||||||
cy.get(".mx_RoomHeader_topic").contains(topic);
|
cy.get(".mx_RoomHeader_topic").contains(topic);
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,13 +16,15 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { synapseDocker } from "./synapsedocker";
|
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
|
import { performance } from "./performance";
|
||||||
|
import { synapseDocker } from "./synapsedocker";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Cypress.PluginConfig}
|
* @type {Cypress.PluginConfig}
|
||||||
*/
|
*/
|
||||||
export default function(on: PluginEvents, config: PluginConfigOptions) {
|
export default function(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
|
performance(on, config);
|
||||||
synapseDocker(on, config);
|
synapseDocker(on, config);
|
||||||
}
|
}
|
||||||
|
|
47
cypress/plugins/performance.ts
Normal file
47
cypress/plugins/performance.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
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 * as path from "path";
|
||||||
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
|
|
||||||
|
// This holds all the performance measurements throughout the run
|
||||||
|
let bufferedMeasurements: PerformanceEntry[] = [];
|
||||||
|
|
||||||
|
function addMeasurements(measurements: PerformanceEntry[]): void {
|
||||||
|
bufferedMeasurements = bufferedMeasurements.concat(measurements);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeMeasurementsFile() {
|
||||||
|
try {
|
||||||
|
const measurementsPath = path.join("cypress", "performance", "measurements.json");
|
||||||
|
await fse.outputJSON(measurementsPath, bufferedMeasurements, {
|
||||||
|
spaces: 4,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
bufferedMeasurements = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function performance(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
|
on("task", { addMeasurements });
|
||||||
|
on("after:run", writeMeasurementsFile);
|
||||||
|
}
|
|
@ -201,7 +201,7 @@ async function synapseStop(id: string): Promise<void> {
|
||||||
synapses.delete(id);
|
synapses.delete(id);
|
||||||
|
|
||||||
console.log(`Stopped synapse id ${id}.`);
|
console.log(`Stopped synapse id ${id}.`);
|
||||||
// cypres deliberately fails if you return 'undefined', so
|
// cypress deliberately fails if you return 'undefined', so
|
||||||
// return null to signal all is well and we've handled the task.
|
// return null to signal all is well and we've handled the task.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string):
|
||||||
const username = Cypress._.uniqueId("userId_");
|
const username = Cypress._.uniqueId("userId_");
|
||||||
const password = Cypress._.uniqueId("password_");
|
const password = Cypress._.uniqueId("password_");
|
||||||
return cy.registerUser(synapse, username, password, displayName).then(credentials => {
|
return cy.registerUser(synapse, username, password, displayName).then(credentials => {
|
||||||
return cy.window().then(win => {
|
return cy.window({ log: false }).then(win => {
|
||||||
const cli = new win.matrixcs.MatrixClient({
|
const cli = new win.matrixcs.MatrixClient({
|
||||||
baseUrl: synapse.baseUrl,
|
baseUrl: synapse.baseUrl,
|
||||||
userId: credentials.userId,
|
userId: credentials.userId,
|
||||||
|
|
|
@ -46,11 +46,11 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => {
|
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => {
|
||||||
return cy.window().then(win => win.mxMatrixClientPeg.matrixClient);
|
return cy.window({ log: false }).then(win => win.mxMatrixClientPeg.matrixClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
|
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
|
||||||
return cy.window().then(async win => {
|
return cy.window({ log: false }).then(async win => {
|
||||||
const cli = win.mxMatrixClientPeg.matrixClient;
|
const cli = win.mxMatrixClientPeg.matrixClient;
|
||||||
const resp = await cli.createRoom(options);
|
const resp = await cli.createRoom(options);
|
||||||
const roomId = resp.room_id;
|
const roomId = resp.room_id;
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import "./performance";
|
||||||
import "./synapse";
|
import "./synapse";
|
||||||
import "./login";
|
import "./login";
|
||||||
import "./client";
|
import "./client";
|
||||||
|
|
|
@ -43,7 +43,7 @@ declare global {
|
||||||
|
|
||||||
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
|
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
|
||||||
// XXX: work around Cypress not clearing IDB between tests
|
// XXX: work around Cypress not clearing IDB between tests
|
||||||
cy.window().then(win => {
|
cy.window({ log: false }).then(win => {
|
||||||
win.indexedDB.databases().then(databases => {
|
win.indexedDB.databases().then(databases => {
|
||||||
databases.forEach(database => {
|
databases.forEach(database => {
|
||||||
win.indexedDB.deleteDatabase(database.name);
|
win.indexedDB.deleteDatabase(database.name);
|
||||||
|
@ -73,7 +73,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
cy.window().then(win => {
|
cy.window({ log: false }).then(win => {
|
||||||
// Seed the localStorage with the required credentials
|
// Seed the localStorage with the required credentials
|
||||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||||
win.localStorage.setItem("mx_user_id", response.body.user_id);
|
win.localStorage.setItem("mx_user_id", response.body.user_id);
|
||||||
|
|
74
cypress/support/performance.ts
Normal file
74
cypress/support/performance.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
import AUTWindow = Cypress.AUTWindow;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
/**
|
||||||
|
* Start measuring the duration of some task.
|
||||||
|
* @param task The task name.
|
||||||
|
*/
|
||||||
|
startMeasuring(task: string): Chainable<AUTWindow>;
|
||||||
|
/**
|
||||||
|
* Stop measuring the duration of some task.
|
||||||
|
* The duration is reported in the Cypress log.
|
||||||
|
* @param task The task name.
|
||||||
|
*/
|
||||||
|
stopMeasuring(task: string): Chainable<AUTWindow>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrefix(task: string): string {
|
||||||
|
return `cy:${Cypress.spec.name.split(".")[0]}:${task}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMeasuring(task: string): Chainable<AUTWindow> {
|
||||||
|
return cy.window({ log: false }).then((win) => {
|
||||||
|
win.mxPerformanceMonitor.start(getPrefix(task));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopMeasuring(task: string): Chainable<AUTWindow> {
|
||||||
|
return cy.window({ log: false }).then((win) => {
|
||||||
|
const measure = win.mxPerformanceMonitor.stop(getPrefix(task));
|
||||||
|
cy.log(`**${task}** ${measure.duration} ms`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("startMeasuring", startMeasuring);
|
||||||
|
Cypress.Commands.add("stopMeasuring", stopMeasuring);
|
||||||
|
|
||||||
|
Cypress.on("window:before:unload", (event: BeforeUnloadEvent) => {
|
||||||
|
const doc = event.target as Document;
|
||||||
|
if (doc.location.href === "about:blank") return;
|
||||||
|
const win = doc.defaultView as AUTWindow;
|
||||||
|
if (!win.mxPerformanceMonitor) return;
|
||||||
|
const entries = win.mxPerformanceMonitor.getEntries().filter(entry => {
|
||||||
|
return entry.name.startsWith("cy:");
|
||||||
|
});
|
||||||
|
if (!entries || entries.length === 0) return;
|
||||||
|
cy.task("addMeasurements", entries);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needed to make this file a module
|
||||||
|
export { };
|
|
@ -63,7 +63,7 @@ function startSynapse(template: string): Chainable<SynapseInstance> {
|
||||||
function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
|
function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
|
||||||
if (!synapse) return;
|
if (!synapse) return;
|
||||||
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
||||||
return cy.window().then((win) => {
|
return cy.window({ log: false }).then((win) => {
|
||||||
win.location.href = 'about:blank';
|
win.location.href = 'about:blank';
|
||||||
cy.task("synapseStop", synapse.synapseId);
|
cy.task("synapseStop", synapse.synapseId);
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default class PerformanceMonitor {
|
||||||
* with the start marker
|
* with the start marker
|
||||||
* @param name Name of the recording
|
* @param name Name of the recording
|
||||||
* @param id Specify an identifier appended to the measurement name
|
* @param id Specify an identifier appended to the measurement name
|
||||||
* @returns {void}
|
* @returns The measurement
|
||||||
*/
|
*/
|
||||||
stop(name: string, id?: string): PerformanceEntry {
|
stop(name: string, id?: string): PerformanceEntry {
|
||||||
if (!this.supportsPerformanceApi()) {
|
if (!this.supportsPerformanceApi()) {
|
||||||
|
|
Loading…
Reference in a new issue