mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-11-20 03:25:52 +01:00
Convert Cypress --> Playwright
This commit is contained in:
parent
a10025ddb2
commit
3ee26e14d7
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,5 +10,4 @@ lib
|
||||
*.tar.gz
|
||||
.eslintcache
|
||||
.tmp
|
||||
cypress/videos
|
||||
cypress/synapselogs
|
||||
playwright/synapselogs
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
@ -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);
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"downlevelIteration": true,
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress", "node"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
@ -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",
|
||||
|
16
playwright.config.ts
Normal file
16
playwright.config.ts
Normal 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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<string, DexInstance>();
|
||||
let env;
|
||||
|
||||
async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
|
||||
const templateDir = path.join(__dirname, "template");
|
||||
@ -56,24 +53,26 @@ async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
|
||||
// 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<DexInstance> {
|
||||
export async function dexStart(): Promise<DexInstance> {
|
||||
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<DexInstance> {
|
||||
return dex;
|
||||
}
|
||||
|
||||
async function dexStop(id: string): Promise<void> {
|
||||
export async function dexStop(id: string): Promise<void> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
}
|
@ -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<string, SynapseInstance>();
|
||||
let env;
|
||||
|
||||
function randB64Bytes(numBytes: number): string {
|
||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||
@ -56,7 +54,7 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||
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<SynapseConfig> {
|
||||
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<SynapseConfig> {
|
||||
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<SynapseConfig> {
|
||||
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<SynapseConfig> {
|
||||
// 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<SynapseInstance> {
|
||||
export async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||
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<SynapseInstance> {
|
||||
return synapse;
|
||||
}
|
||||
|
||||
async function synapseStop(id: string): Promise<void> {
|
||||
export async function synapseStop(id: string): Promise<void> {
|
||||
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<void> {
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
on("before:run", async () => {
|
||||
// tidy up old synapse log files before each run
|
||||
await fse.emptyDir(path.join("cypress", "synapselogs"));
|
||||
});
|
||||
export async function registerUser(synapse: SynapseInstance, username: string, password: string, displayName?: string,): Promise<Credentials> {
|
||||
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,
|
||||
}
|
||||
})).json();
|
||||
return {
|
||||
homeServer: response.home_server,
|
||||
accessToken: response.access_token,
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
};
|
||||
}
|
59
playwright/tests/login.spec.ts
Normal file
59
playwright/tests/login.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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();
|
||||
});
|
13
yarn.lock
13
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"
|
||||
|
Loading…
Reference in New Issue
Block a user