Convert Cypress --> Playwright

This commit is contained in:
RMidhunSuresh 2022-10-18 23:44:58 +05:30
parent a10025ddb2
commit 3ee26e14d7
No known key found for this signature in database
34 changed files with 164 additions and 572 deletions

3
.gitignore vendored
View File

@ -10,5 +10,4 @@ lib
*.tar.gz *.tar.gz
.eslintcache .eslintcache
.tmp .tmp
cypress/videos playwright/synapselogs
cypress/synapselogs

View File

@ -1,17 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
require("./cypress/plugins/index.ts").default(on, config);
return config;
},
baseUrl: "http://127.0.0.1:3000",
},
env: {
SYNAPSE_IP_ADDRESS: "172.18.0.5",
SYNAPSE_PORT: "8008",
DEX_IP_ADDRESS: "172.18.0.4",
DEX_PORT: "5556",
},
});

View File

@ -1,87 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { DexInstance } from "../plugins/dex";
import type { SynapseInstance } from "../plugins/synapsedocker";
describe("Login", () => {
let synapse: SynapseInstance;
let dex: DexInstance;
beforeEach(() => {
cy.startDex().then((data) => {
dex = data;
cy.startSynapse("sso").then((data) => {
synapse = data;
});
});
});
afterEach(() => {
cy.stopSynapse(synapse);
cy.stopDex(dex);
})
it("Login using username/password", () => {
const username = "foobaraccount";
const password = "password123";
cy.registerUser(synapse, username, password);
cy.visit("/");
cy.get("#homeserver").clear().type(synapse.baseUrl);
cy.get("#username").clear().type(username);
cy.get("#password").clear().type(password);
cy.contains("Log In").click();
cy.get(".SessionView");
});
it("Login using SSO", () => {
/**
* Add the homeserver to the localStorage manually; clicking on the start sso button would normally do this but we can't
* use two different origins in a single cypress test!
*/
cy.visit("/");
cy.window().then(win => win.localStorage.setItem("hydrogen_setting_v1_sso_ongoing_login_homeserver", synapse.baseUrl));
// Perform the SSO login manually using requests
const synapseAddress = synapse.baseUrl;
const dexAddress = dex.baseUrl;
// const dexAddress = `${Cypress.env("DEX_IP_ADDRESS")}:${Cypress.env("DEX_PORT")}`;
const redirectAddress = Cypress.config().baseUrl;
const ssoLoginUrl = `${synapseAddress}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(redirectAddress)}`;
cy.request(ssoLoginUrl).then(response => {
// Request the Dex page
const dexPageHtml = response.body;
const loginWithExampleLink = Cypress.$(dexPageHtml).find(`a:contains("Log in with Example")`).attr("href");
cy.log("Login with example link", loginWithExampleLink);
// Proceed to next page
cy.request(`${dexAddress}${loginWithExampleLink}`).then(response => {
const secondDexPageHtml = response.body;
// This req token is used to approve this login in Dex
const req = Cypress.$(secondDexPageHtml).find(`input[name=req]`).attr("value");
cy.log("req for sso login", req);
// Next request will redirect us back to Synapse page with "Continue" link
cy.request("POST", `${dexAddress}/dex/approval?req=${req}&approval=approve`).then(response => {
const synapseHtml = response.body;
const hydrogenLinkWithToken = Cypress.$(synapseHtml).find(`a:contains("Continue")`).attr("href");
cy.log("SSO redirect link", hydrogenLinkWithToken);
cy.visit(hydrogenLinkWithToken);
cy.get(".SessionView");
});
});
});
})
});

View File

@ -1,38 +0,0 @@
/*
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 PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { performance } from "./performance";
import { synapseDocker } from "./synapsedocker";
import { dexDocker } from "./dex";
import { webserver } from "./webserver";
import { docker } from "./docker";
import { log } from "./log";
/**
* @type {Cypress.PluginConfig}
*/
export default function(on: PluginEvents, config: PluginConfigOptions) {
docker(on, config);
performance(on, config);
synapseDocker(on, config);
dexDocker(on, config);
webserver(on, config);
log(on, config);
}

View File

@ -1,35 +0,0 @@
/*
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 PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
export function log(on: PluginEvents, config: PluginConfigOptions) {
on("task", {
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
return null;
},
});
}

View File

@ -1,47 +0,0 @@
/*
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);
}

View File

@ -1,52 +0,0 @@
/*
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 http from "http";
import { AddressInfo } from "net";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
const servers: http.Server[] = [];
function serveHtmlFile(html: string): string {
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/html",
});
res.end(html);
});
server.listen();
servers.push(server);
return `http://localhost:${(server.address() as AddressInfo).port}/`;
}
function stopWebServers(): null {
for (const server of servers) {
server.close();
}
servers.splice(0, servers.length); // clear
return null; // tell cypress we did the task successfully (doesn't allow undefined)
}
export function webserver(on: PluginEvents, config: PluginConfigOptions) {
on("task", { serveHtmlFile, stopWebServers });
on("after:run", stopWebServers);
}

View File

@ -1,51 +0,0 @@
/*
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;
import { DexInstance } from "../plugins/dex";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start the dex server
*/
startDex(): Chainable<DexInstance>;
/**
* Stop the dex server
* @param dex the dex instance returned by startSynapse
*/
stopDex(dex: DexInstance): Chainable<AUTWindow>;
}
}
}
function startDex(): Chainable<DexInstance> {
return cy.task<DexInstance>("dexStart");
}
function stopDex(dex?: DexInstance): Chainable<AUTWindow> {
if (!dex) return;
cy.task("dexStop", dex.dexId);
}
Cypress.Commands.add("startDex", startDex);
Cypress.Commands.add("stopDex", stopDex);

View File

@ -1,122 +0,0 @@
/*
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 crypto from 'crypto';
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
import { SynapseInstance } from "../plugins/synapsedocker";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start a synapse instance with a given config template.
* @param template path to template within cypress/plugins/synapsedocker/template/ directory.
*/
startSynapse(template: string): Chainable<SynapseInstance>;
/**
* Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
* for if Synapse stopping races with the app's background sync loop.
* @param synapse the synapse instance returned by startSynapse
*/
stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow>;
/**
* Register a user on the given Synapse using the shared registration secret.
* @param synapse the synapse instance returned by startSynapse
* @param username the username of the user to register
* @param password the password of the user to register
* @param displayName optional display name to set on the newly registered user
*/
registerUser(
synapse: SynapseInstance,
username: string,
password: string,
displayName?: string,
): Chainable<Credentials>;
}
}
}
function startSynapse(template: string): Chainable<SynapseInstance> {
return cy.task<SynapseInstance>("synapseStart", template);
}
function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
if (!synapse) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window({ log: false }).then((win) => {
win.location.href = 'about:blank';
cy.task("synapseStop", synapse.synapseId);
});
}
interface Credentials {
accessToken: string;
userId: string;
deviceId: string;
homeServer: string;
}
function registerUser(
synapse: SynapseInstance,
username: string,
password: string,
displayName?: string,
): Chainable<Credentials> {
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
return cy.then(() => {
// get a nonce
return cy.request<{ nonce: string }>({ url });
}).then(response => {
const { nonce } = response.body;
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
`${nonce}\0${username}\0${password}\0notadmin`,
).digest('hex');
return cy.request<{
access_token: string;
user_id: string;
home_server: string;
device_id: string;
}>({
url,
method: "POST",
body: {
nonce,
username,
password,
mac,
admin: false,
displayname: displayName,
},
});
}).then(response => ({
homeServer: response.body.home_server,
accessToken: response.body.access_token,
userId: response.body.user_id,
deviceId: response.body.device_id,
}));
}
Cypress.Commands.add("startSynapse", startSynapse);
Cypress.Commands.add("stopSynapse", stopSynapse);
Cypress.Commands.add("registerUser", registerUser);

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"downlevelIteration": true,
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

View File

@ -19,7 +19,7 @@
"build": "vite build && ./scripts/cleanup.sh", "build": "vite build && ./scripts/cleanup.sh",
"build:sdk": "./scripts/sdk/build.sh", "build:sdk": "./scripts/sdk/build.sh",
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch", "watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch",
"test:app": "vite --port 3000 & yarn run cypress open" "test:app": "vite --port 3000 & playwright test"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -32,6 +32,7 @@
}, },
"homepage": "https://github.com/vector-im/hydrogen-web/#readme", "homepage": "https://github.com/vector-im/hydrogen-web/#readme",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.27.1",
"@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2", "@typescript-eslint/parser": "^4.29.2",
"acorn": "^8.6.0", "acorn": "^8.6.0",

16
playwright.config.ts Normal file
View File

@ -0,0 +1,16 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const BASE_URL = process.env["BASE_URL"] ?? "http://127.0.0.1:3000";
const config: PlaywrightTestConfig = {
use: {
headless: false,
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
video: 'on-first-retry',
baseURL: BASE_URL,
},
testDir: "./playwright/tests",
globalSetup: require.resolve("./playwright/global-setup"),
};
export default config;

View File

@ -14,9 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
describe("App has no startup errors that prevent UI render", () => { const env = {
it("passes", () => { SYNAPSE_IP_ADDRESS: "172.18.0.5",
cy.visit("/"); SYNAPSE_PORT: "8008",
cy.contains("Log In"); DEX_IP_ADDRESS: "172.18.0.4",
}) DEX_PORT: "5556",
}) }
export default function setupEnvironmentVariables() {
for (const [key, value] of Object.entries(env)) {
process.env[key] = value;
}
}

View File

@ -20,8 +20,6 @@ import * as path from "path";
import * as os from "os"; import * as os from "os";
import * as fse from "fs-extra"; import * as fse from "fs-extra";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import {dockerRun, dockerStop } from "../docker"; import {dockerRun, dockerStop } from "../docker";
// A cypress plugins to add command to start & stop dex instances // A cypress plugins to add command to start & stop dex instances
@ -38,7 +36,6 @@ export interface DexInstance extends DexConfig {
} }
const dexConfigs = new Map<string, DexInstance>(); const dexConfigs = new Map<string, DexInstance>();
let env;
async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> { async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
const templateDir = path.join(__dirname, "template"); const templateDir = path.join(__dirname, "template");
@ -56,24 +53,26 @@ async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
// now copy config.yaml, applying substitutions // now copy config.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, "config.yaml")}`); console.log(`Gen ${path.join(templateDir, "config.yaml")}`);
let hsYaml = await fse.readFile(path.join(templateDir, "config.yaml"), "utf8"); let hsYaml = await fse.readFile(path.join(templateDir, "config.yaml"), "utf8");
const synapseAddress = `${env.SYNAPSE_IP_ADDRESS}:${env.SYNAPSE_PORT}`; const synapseHost = process.env.SYNAPSE_IP_ADDRESS;
const synapsePort = process.env.SYNAPSE_PORT;
const synapseAddress = `${synapseHost}:${synapsePort}`;
hsYaml = hsYaml.replace(/{{SYNAPSE_ADDRESS}}/g, synapseAddress); hsYaml = hsYaml.replace(/{{SYNAPSE_ADDRESS}}/g, synapseAddress);
const host = env.DEX_IP_ADDRESS; const dexHost = process.env.DEX_IP_ADDRESS!;
const port = env.DEX_PORT; const dexPort = parseInt(process.env.DEX_PORT!, 10);
const dexAddress = `${host}:${port}`; const dexAddress = `${dexHost}:${dexPort}`;
hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress); hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress);
await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml); await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml);
const baseUrl = `http://${host}:${port}`; const baseUrl = `http://${dexHost}:${dexPort}`;
return { return {
host, host: dexHost,
port, port: dexPort,
baseUrl, baseUrl,
configDir: tempDir, configDir: tempDir,
}; };
} }
async function dexStart(): Promise<DexInstance> { export async function dexStart(): Promise<DexInstance> {
const dexCfg = await produceConfigWithSynapseURLAdded(); const dexCfg = await produceConfigWithSynapseURLAdded();
console.log(`Starting dex with config dir ${dexCfg.configDir}...`); console.log(`Starting dex with config dir ${dexCfg.configDir}...`);
const dexId = await dockerRun({ const dexId = await dockerRun({
@ -99,34 +98,11 @@ async function dexStart(): Promise<DexInstance> {
return dex; return dex;
} }
async function dexStop(id: string): Promise<void> { export async function dexStop(id: string): Promise<void> {
const dexCfg = dexConfigs.get(id); const dexCfg = dexConfigs.get(id);
if (!dexCfg) throw new Error("Unknown dex ID"); if (!dexCfg) throw new Error("Unknown dex ID");
await dockerStop({ containerId: id, }); await dockerStop({ containerId: id, });
await fse.remove(dexCfg.configDir); await fse.remove(dexCfg.configDir);
dexConfigs.delete(id); dexConfigs.delete(id);
console.log(`Stopped dex id ${id}.`); console.log(`Stopped dex id ${id}.`);
// cypress deliberately fails if you return 'undefined', so
// return null to signal all is well, and we've handled the task.
return null;
} }
/**
* @type {Cypress.PluginConfig}
*/
export function dexDocker(on: PluginEvents, config: PluginConfigOptions) {
env = config.env;
on("task", {
dexStart,
dexStop,
});
on("after:spec", async (spec) => {
for (const dexId of dexConfigs.keys()) {
console.warn(`Cleaning up dex ID ${dexId} after ${spec.name}`);
await dexStop(dexId);
}
});
}

View File

@ -20,11 +20,6 @@ import * as os from "os";
import * as childProcess from "child_process"; import * as childProcess from "child_process";
import * as fse from "fs-extra"; import * as fse from "fs-extra";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
// A cypress plugin to run docker commands
export function dockerRun(args: { export function dockerRun(args: {
image: string; image: string;
containerName: string; containerName: string;
@ -48,8 +43,7 @@ export function dockerRun(args: {
...params, ...params,
args.image, args.image,
... appParams ... appParams
], (err, stdout, stderr) => { ], (err, stdout) => {
console.log("error", err, "stdout", stdout, "stderr", stderr);
if (err) { if (err) {
reject(err); reject(err);
} }
@ -155,18 +149,3 @@ export function dockerRm(args: {
}); });
}); });
} }
/**
* @type {Cypress.PluginConfig}
*/
export function docker(on: PluginEvents, config: PluginConfigOptions) {
console.log("Code gets to here!");
on("task", {
dockerRun,
dockerExec,
dockerLogs,
dockerStop,
dockerRm,
dockerCreateNetwork
});
}

View File

@ -21,9 +21,8 @@ import * as os from "os";
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as fse from "fs-extra"; import * as fse from "fs-extra";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
import { request } from "@playwright/test";
// A cypress plugins to add command to start & stop synapses in // A cypress plugins to add command to start & stop synapses in
@ -43,7 +42,6 @@ export interface SynapseInstance extends SynapseConfig {
} }
const synapses = new Map<string, SynapseInstance>(); const synapses = new Map<string, SynapseInstance>();
let env;
function randB64Bytes(numBytes: number): string { function randB64Bytes(numBytes: number): string {
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
@ -56,7 +54,7 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
if (!stats?.isDirectory) { if (!stats?.isDirectory) {
throw new Error(`No such template: ${template}`); throw new Error(`No such template: ${template}`);
} }
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-')); const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'synapsedocker-'));
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that // copy the contents of the template dir, omitting homeserver.yaml as we'll template that
console.log(`Copy ${templateDir} -> ${tempDir}`); console.log(`Copy ${templateDir} -> ${tempDir}`);
@ -66,9 +64,10 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
const macaroonSecret = randB64Bytes(16); const macaroonSecret = randB64Bytes(16);
const formSecret = randB64Bytes(16); const formSecret = randB64Bytes(16);
const host = env["SYNAPSE_IP_ADDRESS"]; const synapseHost = process.env["SYNAPSE_IP_ADDRESS"]!!;
const port = parseInt(env["SYNAPSE_PORT"], 10); const synapsePort = parseInt(process.env["SYNAPSE_PORT"]!, 10);
const baseUrl = `http://${host}:${port}`; const baseUrl = `http://${synapseHost}:${synapsePort}`;
// now copy homeserver.yaml, applying substitutions // now copy homeserver.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`); console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
@ -77,7 +76,10 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
const dexUrl = `http://${env["DEX_IP_ADDRESS"]}:${env["DEX_PORT"]}/dex`;
const dexHost = process.env["DEX_IP_ADDRESS"];
const dexPort = process.env["DEX_PORT"];
const dexUrl = `http://${dexHost}:${dexPort}/dex`;
hsYaml = hsYaml.replace(/{{OIDC_ISSUER}}/g, dexUrl); hsYaml = hsYaml.replace(/{{OIDC_ISSUER}}/g, dexUrl);
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
@ -89,8 +91,8 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`); await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
return { return {
port, port: synapsePort,
host, host: synapseHost,
baseUrl, baseUrl,
configDir: tempDir, configDir: tempDir,
registrationSecret, registrationSecret,
@ -100,7 +102,7 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
// Start a synapse instance: the template must be the name of // Start a synapse instance: the template must be the name of
// one of the templates in the cypress/plugins/synapsedocker/templates // one of the templates in the cypress/plugins/synapsedocker/templates
// directory // directory
async function synapseStart(template: string): Promise<SynapseInstance> { export async function synapseStart(template: string): Promise<SynapseInstance> {
const synCfg = await cfgDirFromTemplate(template); const synCfg = await cfgDirFromTemplate(template);
console.log(`Starting synapse with config dir ${synCfg.configDir}...`); console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
await dockerCreateNetwork({ networkName: "hydrogen" }); await dockerCreateNetwork({ networkName: "hydrogen" });
@ -143,12 +145,12 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
return synapse; return synapse;
} }
async function synapseStop(id: string): Promise<void> { export async function synapseStop(id: string): Promise<void> {
const synCfg = synapses.get(id); const synCfg = synapses.get(id);
if (!synCfg) throw new Error("Unknown synapse ID"); if (!synCfg) throw new Error("Unknown synapse ID");
const synapseLogsPath = path.join("cypress", "synapselogs", id); const synapseLogsPath = path.join("playwright", "synapselogs", id);
await fse.ensureDir(synapseLogsPath); await fse.ensureDir(synapseLogsPath);
await dockerLogs({ await dockerLogs({
@ -162,42 +164,40 @@ async function synapseStop(id: string): Promise<void> {
}); });
await fse.remove(synCfg.configDir); await fse.remove(synCfg.configDir);
synapses.delete(id); synapses.delete(id);
console.log(`Stopped synapse id ${id}.`); console.log(`Stopped synapse id ${id}.`);
// cypress deliberately fails if you return 'undefined', so
// return null to signal all is well, and we've handled the task.
return null;
} }
/**
* @type {Cypress.PluginConfig}
*/
export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) {
env = config.env;
on("task", {
synapseStart,
synapseStop,
});
on("after:spec", async (spec) => { interface Credentials {
// Cleans up any remaining synapse instances after a spec run accessToken: string;
// This is on the theory that we should avoid re-using synapse userId: string;
// instances between spec runs: they should be cheap enough to deviceId: string;
// start that we can have a separate one for each spec run or even homeServer: string;
// test. If we accidentally re-use synapses, we could inadvertently }
// make our tests depend on each other.
for (const synId of synapses.keys()) { export async function registerUser(synapse: SynapseInstance, username: string, password: string, displayName?: string,): Promise<Credentials> {
console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
await synapseStop(synId); const context = await request.newContext({ baseURL: url });
const { nonce } = await (await context.get(url)).json();
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
`${nonce}\0${username}\0${password}\0notadmin`,
).digest('hex');
const response = await (await context.post(url, {
data: {
nonce,
username,
password,
mac,
admin: false,
displayname: displayName,
} }
}); })).json();
return {
on("before:run", async () => { homeServer: response.home_server,
// tidy up old synapse log files before each run accessToken: response.access_token,
await fse.emptyDir(path.join("cypress", "synapselogs")); userId: response.user_id,
}); deviceId: response.device_id,
};
} }

View File

@ -0,0 +1,59 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test } from '@playwright/test';
import { synapseStart, synapseStop, registerUser } from "../plugins/synapsedocker";
import { dexStart, dexStop } from "../plugins/dex";
import type { DexInstance } from "../plugins/dex";
import type { SynapseInstance } from "../plugins/synapsedocker";
test.describe("Login", () => {
let synapse: SynapseInstance;
let dex: DexInstance;
test.beforeEach(async () => {
dex = await dexStart();
synapse = await synapseStart("sso");
});
test.afterEach(async () => {
await synapseStop(synapse.synapseId);
await dexStop(dex.dexId);
});
test("Login using username/password", async ({ page }) => {
const username = "foobaraccount";
const password = "password123";
await registerUser(synapse, username, password);
await page.goto("/");
await page.locator("#homeserver").fill("");
await page.locator("#homeserver").type(synapse.baseUrl);
await page.locator("#username").type(username);
await page.locator("#password").type(password);
await page.getByText('Log In', { exact: true }).click();
await page.locator(".SessionView").waitFor();
});
test("Login using SSO", async ({ page }) => {
await page.goto("/");
await page.locator("#homeserver").fill("");
await page.locator("#homeserver").type(synapse.baseUrl);
await page.locator(".StartSSOLoginView_button").click();
await page.getByText("Log in with Example").click();
await page.locator(".dex-btn-text", {hasText: "Grant Access"}).click();
await page.locator(".primary-button", {hasText: "Continue"}).click();
await page.locator(".SessionView").waitFor();
});
});

View File

@ -13,8 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { test } from '@playwright/test';
/// <reference types="cypress" /> test("App has no startup errors that prevent UI render", async ({ page }) => {
await page.goto('/');
import "./synapse"; await page.getByText('Log In', { exact: true }).waitFor();
import "./dex"; });

View File

@ -114,6 +114,14 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@playwright/test@^1.27.1":
version "1.27.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.27.1.tgz#9364d1e02021261211c8ff586d903faa79ce95c4"
integrity sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==
dependencies:
"@types/node" "*"
playwright-core "1.27.1"
"@trysound/sax@0.2.0": "@trysound/sax@0.2.0":
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
@ -2040,6 +2048,11 @@ pify@^2.2.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
playwright-core@1.27.1:
version "1.27.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.27.1.tgz#840ef662e55a3ed759d8b5d3d00a5f885a7184f4"
integrity sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==
postcss-css-variables@^0.18.0: postcss-css-variables@^0.18.0:
version "0.18.0" version "0.18.0"
resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93" resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93"