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"