diff --git a/.gitignore b/.gitignore
index 20f09f19..ef4f67ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,5 +10,4 @@ lib
*.tar.gz
.eslintcache
.tmp
-cypress/videos
-cypress/synapselogs
+playwright/synapselogs
diff --git a/cypress.config.ts b/cypress.config.ts
deleted file mode 100644
index 02f39392..00000000
--- a/cypress.config.ts
+++ /dev/null
@@ -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",
- },
-});
diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts
deleted file mode 100644
index 969c1884..00000000
--- a/cypress/e2e/login.cy.ts
+++ /dev/null
@@ -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");
- });
- });
- });
- })
-});
-
diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts
deleted file mode 100644
index 27c8b9a3..00000000
--- a/cypress/plugins/index.ts
+++ /dev/null
@@ -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.
-*/
-
-///
-
-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);
-}
diff --git a/cypress/plugins/log.ts b/cypress/plugins/log.ts
deleted file mode 100644
index 4b16c9b8..00000000
--- a/cypress/plugins/log.ts
+++ /dev/null
@@ -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.
-*/
-
-///
-
-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;
- },
- });
-}
diff --git a/cypress/plugins/performance.ts b/cypress/plugins/performance.ts
deleted file mode 100644
index c6bd3e4c..00000000
--- a/cypress/plugins/performance.ts
+++ /dev/null
@@ -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.
-*/
-
-///
-
-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);
-}
diff --git a/cypress/plugins/webserver.ts b/cypress/plugins/webserver.ts
deleted file mode 100644
index 55a25a31..00000000
--- a/cypress/plugins/webserver.ts
+++ /dev/null
@@ -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.
-*/
-
-///
-
-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);
-}
diff --git a/cypress/support/dex.ts b/cypress/support/dex.ts
deleted file mode 100644
index 599eee26..00000000
--- a/cypress/support/dex.ts
+++ /dev/null
@@ -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.
-*/
-
-///
-
-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;
-
- /**
- * Stop the dex server
- * @param dex the dex instance returned by startSynapse
- */
- stopDex(dex: DexInstance): Chainable;
- }
- }
-}
-
-function startDex(): Chainable {
- return cy.task("dexStart");
-}
-
-function stopDex(dex?: DexInstance): Chainable {
- if (!dex) return;
- cy.task("dexStop", dex.dexId);
-}
-
-Cypress.Commands.add("startDex", startDex);
-Cypress.Commands.add("stopDex", stopDex);
diff --git a/cypress/support/synapse.ts b/cypress/support/synapse.ts
deleted file mode 100644
index 5696e8c0..00000000
--- a/cypress/support/synapse.ts
+++ /dev/null
@@ -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.
-*/
-
-///
-
-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;
-
- /**
- * 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;
-
- /**
- * 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;
- }
- }
-}
-
-function startSynapse(template: string): Chainable {
- return cy.task("synapseStart", template);
-}
-
-function stopSynapse(synapse?: SynapseInstance): Chainable {
- 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 {
- 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);
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
deleted file mode 100644
index 6bd44918..00000000
--- a/cypress/tsconfig.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "compilerOptions": {
- "downlevelIteration": true,
- "target": "es5",
- "lib": ["es5", "dom"],
- "types": ["cypress", "node"]
- },
- "include": ["**/*.ts"]
-}
diff --git a/package.json b/package.json
index fbba3b2e..569354f0 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"build": "vite build && ./scripts/cleanup.sh",
"build:sdk": "./scripts/sdk/build.sh",
"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": {
"type": "git",
@@ -32,6 +32,7 @@
},
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
"devDependencies": {
+ "@playwright/test": "^1.27.1",
"@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2",
"acorn": "^8.6.0",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..901e48d9
--- /dev/null
+++ b/playwright.config.ts
@@ -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;
diff --git a/cypress/e2e/startup.cy.ts b/playwright/global-setup.ts
similarity index 66%
rename from cypress/e2e/startup.cy.ts
rename to playwright/global-setup.ts
index e7bf9c2c..5944e55c 100644
--- a/cypress/e2e/startup.cy.ts
+++ b/playwright/global-setup.ts
@@ -14,9 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-describe("App has no startup errors that prevent UI render", () => {
- it("passes", () => {
- cy.visit("/");
- cy.contains("Log In");
- })
-})
+const env = {
+ SYNAPSE_IP_ADDRESS: "172.18.0.5",
+ SYNAPSE_PORT: "8008",
+ 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;
+ }
+}
diff --git a/cypress/plugins/dex/index.ts b/playwright/plugins/dex/index.ts
similarity index 74%
rename from cypress/plugins/dex/index.ts
rename to playwright/plugins/dex/index.ts
index b07a78e8..56605cf5 100644
--- a/cypress/plugins/dex/index.ts
+++ b/playwright/plugins/dex/index.ts
@@ -20,8 +20,6 @@ import * as path from "path";
import * as os from "os";
import * as fse from "fs-extra";
-import PluginEvents = Cypress.PluginEvents;
-import PluginConfigOptions = Cypress.PluginConfigOptions;
import {dockerRun, dockerStop } from "../docker";
// A cypress plugins to add command to start & stop dex instances
@@ -38,7 +36,6 @@ export interface DexInstance extends DexConfig {
}
const dexConfigs = new Map();
-let env;
async function produceConfigWithSynapseURLAdded(): Promise {
const templateDir = path.join(__dirname, "template");
@@ -56,24 +53,26 @@ async function produceConfigWithSynapseURLAdded(): Promise {
// now copy config.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, "config.yaml")}`);
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);
- const host = env.DEX_IP_ADDRESS;
- const port = env.DEX_PORT;
- const dexAddress = `${host}:${port}`;
+ const dexHost = process.env.DEX_IP_ADDRESS!;
+ const dexPort = parseInt(process.env.DEX_PORT!, 10);
+ const dexAddress = `${dexHost}:${dexPort}`;
hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress);
await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml);
- const baseUrl = `http://${host}:${port}`;
+ const baseUrl = `http://${dexHost}:${dexPort}`;
return {
- host,
- port,
+ host: dexHost,
+ port: dexPort,
baseUrl,
configDir: tempDir,
};
}
-async function dexStart(): Promise {
+export async function dexStart(): Promise {
const dexCfg = await produceConfigWithSynapseURLAdded();
console.log(`Starting dex with config dir ${dexCfg.configDir}...`);
const dexId = await dockerRun({
@@ -99,34 +98,11 @@ async function dexStart(): Promise {
return dex;
}
-async function dexStop(id: string): Promise {
+export async function dexStop(id: string): Promise {
const dexCfg = dexConfigs.get(id);
if (!dexCfg) throw new Error("Unknown dex ID");
await dockerStop({ containerId: id, });
await fse.remove(dexCfg.configDir);
dexConfigs.delete(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);
- }
- });
-}
-
diff --git a/cypress/plugins/dex/template/config.yaml b/playwright/plugins/dex/template/config.yaml
similarity index 100%
rename from cypress/plugins/dex/template/config.yaml
rename to playwright/plugins/dex/template/config.yaml
diff --git a/cypress/plugins/dex/template/dev.db b/playwright/plugins/dex/template/dev.db
similarity index 100%
rename from cypress/plugins/dex/template/dev.db
rename to playwright/plugins/dex/template/dev.db
diff --git a/cypress/plugins/docker/index.ts b/playwright/plugins/docker/index.ts
similarity index 88%
rename from cypress/plugins/docker/index.ts
rename to playwright/plugins/docker/index.ts
index a55f341e..b0080d6c 100644
--- a/cypress/plugins/docker/index.ts
+++ b/playwright/plugins/docker/index.ts
@@ -20,11 +20,6 @@ import * as os from "os";
import * as childProcess from "child_process";
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: {
image: string;
containerName: string;
@@ -48,8 +43,7 @@ export function dockerRun(args: {
...params,
args.image,
... appParams
- ], (err, stdout, stderr) => {
- console.log("error", err, "stdout", stdout, "stderr", stderr);
+ ], (err, stdout) => {
if (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
- });
-}
diff --git a/cypress/plugins/synapsedocker/index.ts b/playwright/plugins/synapsedocker/index.ts
similarity index 74%
rename from cypress/plugins/synapsedocker/index.ts
rename to playwright/plugins/synapsedocker/index.ts
index 268f7320..1917aa85 100644
--- a/cypress/plugins/synapsedocker/index.ts
+++ b/playwright/plugins/synapsedocker/index.ts
@@ -21,9 +21,8 @@ import * as os from "os";
import * as crypto from "crypto";
import * as fse from "fs-extra";
-import PluginEvents = Cypress.PluginEvents;
-import PluginConfigOptions = Cypress.PluginConfigOptions;
import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
+import { request } from "@playwright/test";
// A cypress plugins to add command to start & stop synapses in
@@ -43,7 +42,6 @@ export interface SynapseInstance extends SynapseConfig {
}
const synapses = new Map();
-let env;
function randB64Bytes(numBytes: number): string {
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
@@ -56,7 +54,7 @@ async function cfgDirFromTemplate(template: string): Promise {
if (!stats?.isDirectory) {
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
console.log(`Copy ${templateDir} -> ${tempDir}`);
@@ -66,9 +64,10 @@ async function cfgDirFromTemplate(template: string): Promise {
const macaroonSecret = randB64Bytes(16);
const formSecret = randB64Bytes(16);
- const host = env["SYNAPSE_IP_ADDRESS"];
- const port = parseInt(env["SYNAPSE_PORT"], 10);
- const baseUrl = `http://${host}:${port}`;
+ const synapseHost = process.env["SYNAPSE_IP_ADDRESS"]!!;
+ const synapsePort = parseInt(process.env["SYNAPSE_PORT"]!, 10);
+ const baseUrl = `http://${synapseHost}:${synapsePort}`;
+
// now copy homeserver.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
@@ -77,7 +76,10 @@ async function cfgDirFromTemplate(template: string): Promise {
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
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);
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
@@ -89,8 +91,8 @@ async function cfgDirFromTemplate(template: string): Promise {
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
return {
- port,
- host,
+ port: synapsePort,
+ host: synapseHost,
baseUrl,
configDir: tempDir,
registrationSecret,
@@ -100,7 +102,7 @@ async function cfgDirFromTemplate(template: string): Promise {
// Start a synapse instance: the template must be the name of
// one of the templates in the cypress/plugins/synapsedocker/templates
// directory
-async function synapseStart(template: string): Promise {
+export async function synapseStart(template: string): Promise {
const synCfg = await cfgDirFromTemplate(template);
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
await dockerCreateNetwork({ networkName: "hydrogen" });
@@ -143,12 +145,12 @@ async function synapseStart(template: string): Promise {
return synapse;
}
-async function synapseStop(id: string): Promise {
+export async function synapseStop(id: string): Promise {
const synCfg = synapses.get(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 dockerLogs({
@@ -162,42 +164,40 @@ async function synapseStop(id: string): Promise {
});
await fse.remove(synCfg.configDir);
-
synapses.delete(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) => {
- // Cleans up any remaining synapse instances after a spec run
- // This is on the theory that we should avoid re-using synapse
- // instances between spec runs: they should be cheap enough to
- // start that we can have a separate one for each spec run or even
- // test. If we accidentally re-use synapses, we could inadvertently
- // make our tests depend on each other.
- for (const synId of synapses.keys()) {
- console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`);
- await synapseStop(synId);
+interface Credentials {
+ accessToken: string;
+ userId: string;
+ deviceId: string;
+ homeServer: string;
+}
+
+export async function registerUser(synapse: SynapseInstance, username: string, password: string, displayName?: string,): Promise {
+ const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
+ 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,
}
- });
-
- on("before:run", async () => {
- // tidy up old synapse log files before each run
- await fse.emptyDir(path.join("cypress", "synapselogs"));
- });
+ })).json();
+ return {
+ homeServer: response.home_server,
+ accessToken: response.access_token,
+ userId: response.user_id,
+ deviceId: response.device_id,
+ };
}
diff --git a/cypress/plugins/synapsedocker/templates/COPYME/README.md b/playwright/plugins/synapsedocker/templates/COPYME/README.md
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/COPYME/README.md
rename to playwright/plugins/synapsedocker/templates/COPYME/README.md
diff --git a/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml b/playwright/plugins/synapsedocker/templates/COPYME/homeserver.yaml
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml
rename to playwright/plugins/synapsedocker/templates/COPYME/homeserver.yaml
diff --git a/cypress/plugins/synapsedocker/templates/COPYME/log.config b/playwright/plugins/synapsedocker/templates/COPYME/log.config
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/COPYME/log.config
rename to playwright/plugins/synapsedocker/templates/COPYME/log.config
diff --git a/cypress/plugins/synapsedocker/templates/consent/README.md b/playwright/plugins/synapsedocker/templates/consent/README.md
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/consent/README.md
rename to playwright/plugins/synapsedocker/templates/consent/README.md
diff --git a/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml b/playwright/plugins/synapsedocker/templates/consent/homeserver.yaml
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/consent/homeserver.yaml
rename to playwright/plugins/synapsedocker/templates/consent/homeserver.yaml
diff --git a/cypress/plugins/synapsedocker/templates/consent/log.config b/playwright/plugins/synapsedocker/templates/consent/log.config
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/consent/log.config
rename to playwright/plugins/synapsedocker/templates/consent/log.config
diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html b/playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html
rename to playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html
diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html b/playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html
rename to playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html
diff --git a/cypress/plugins/synapsedocker/templates/default/README.md b/playwright/plugins/synapsedocker/templates/default/README.md
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/default/README.md
rename to playwright/plugins/synapsedocker/templates/default/README.md
diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/playwright/plugins/synapsedocker/templates/default/homeserver.yaml
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/default/homeserver.yaml
rename to playwright/plugins/synapsedocker/templates/default/homeserver.yaml
diff --git a/cypress/plugins/synapsedocker/templates/default/log.config b/playwright/plugins/synapsedocker/templates/default/log.config
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/default/log.config
rename to playwright/plugins/synapsedocker/templates/default/log.config
diff --git a/cypress/plugins/synapsedocker/templates/sso/homeserver.yaml b/playwright/plugins/synapsedocker/templates/sso/homeserver.yaml
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/sso/homeserver.yaml
rename to playwright/plugins/synapsedocker/templates/sso/homeserver.yaml
diff --git a/cypress/plugins/synapsedocker/templates/sso/log.config b/playwright/plugins/synapsedocker/templates/sso/log.config
similarity index 100%
rename from cypress/plugins/synapsedocker/templates/sso/log.config
rename to playwright/plugins/synapsedocker/templates/sso/log.config
diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts
new file mode 100644
index 00000000..a7684eae
--- /dev/null
+++ b/playwright/tests/login.spec.ts
@@ -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();
+ });
+});
diff --git a/cypress/support/e2e.ts b/playwright/tests/startup.spec.ts
similarity index 73%
rename from cypress/support/e2e.ts
rename to playwright/tests/startup.spec.ts
index d186c5a0..0d38218a 100644
--- a/cypress/support/e2e.ts
+++ b/playwright/tests/startup.spec.ts
@@ -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
limitations under the License.
*/
+import { test } from '@playwright/test';
-///
-
-import "./synapse";
-import "./dex";
+test("App has no startup errors that prevent UI render", async ({ page }) => {
+ await page.goto('/');
+ await page.getByText('Log In', { exact: true }).waitFor();
+});
diff --git a/yarn.lock b/yarn.lock
index 994657f3..188a5fe1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -114,6 +114,14 @@
"@nodelib/fs.scandir" "2.1.5"
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":
version "0.2.0"
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"
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:
version "0.18.0"
resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93"