mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 11:05:03 +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
|
*.tar.gz
|
||||||
.eslintcache
|
.eslintcache
|
||||||
.tmp
|
.tmp
|
||||||
cypress/videos
|
playwright/synapselogs
|
||||||
cypress/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": "vite build && ./scripts/cleanup.sh",
|
||||||
"build:sdk": "./scripts/sdk/build.sh",
|
"build:sdk": "./scripts/sdk/build.sh",
|
||||||
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch",
|
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch",
|
||||||
"test:app": "vite --port 3000 & yarn run cypress open"
|
"test:app": "vite --port 3000 & playwright test"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.27.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||||
"@typescript-eslint/parser": "^4.29.2",
|
"@typescript-eslint/parser": "^4.29.2",
|
||||||
"acorn": "^8.6.0",
|
"acorn": "^8.6.0",
|
||||||
|
16
playwright.config.ts
Normal file
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe("App has no startup errors that prevent UI render", () => {
|
const env = {
|
||||||
it("passes", () => {
|
SYNAPSE_IP_ADDRESS: "172.18.0.5",
|
||||||
cy.visit("/");
|
SYNAPSE_PORT: "8008",
|
||||||
cy.contains("Log In");
|
DEX_IP_ADDRESS: "172.18.0.4",
|
||||||
})
|
DEX_PORT: "5556",
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export default function setupEnvironmentVariables() {
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
@ -20,8 +20,6 @@ import * as path from "path";
|
|||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as fse from "fs-extra";
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
|
||||||
import {dockerRun, dockerStop } from "../docker";
|
import {dockerRun, dockerStop } from "../docker";
|
||||||
|
|
||||||
// A cypress plugins to add command to start & stop dex instances
|
// A cypress plugins to add command to start & stop dex instances
|
||||||
@ -38,7 +36,6 @@ export interface DexInstance extends DexConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dexConfigs = new Map<string, DexInstance>();
|
const dexConfigs = new Map<string, DexInstance>();
|
||||||
let env;
|
|
||||||
|
|
||||||
async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
|
async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
|
||||||
const templateDir = path.join(__dirname, "template");
|
const templateDir = path.join(__dirname, "template");
|
||||||
@ -56,24 +53,26 @@ async function produceConfigWithSynapseURLAdded(): Promise<DexConfig> {
|
|||||||
// now copy config.yaml, applying substitutions
|
// now copy config.yaml, applying substitutions
|
||||||
console.log(`Gen ${path.join(templateDir, "config.yaml")}`);
|
console.log(`Gen ${path.join(templateDir, "config.yaml")}`);
|
||||||
let hsYaml = await fse.readFile(path.join(templateDir, "config.yaml"), "utf8");
|
let hsYaml = await fse.readFile(path.join(templateDir, "config.yaml"), "utf8");
|
||||||
const synapseAddress = `${env.SYNAPSE_IP_ADDRESS}:${env.SYNAPSE_PORT}`;
|
const synapseHost = process.env.SYNAPSE_IP_ADDRESS;
|
||||||
|
const synapsePort = process.env.SYNAPSE_PORT;
|
||||||
|
const synapseAddress = `${synapseHost}:${synapsePort}`;
|
||||||
hsYaml = hsYaml.replace(/{{SYNAPSE_ADDRESS}}/g, synapseAddress);
|
hsYaml = hsYaml.replace(/{{SYNAPSE_ADDRESS}}/g, synapseAddress);
|
||||||
const host = env.DEX_IP_ADDRESS;
|
const dexHost = process.env.DEX_IP_ADDRESS!;
|
||||||
const port = env.DEX_PORT;
|
const dexPort = parseInt(process.env.DEX_PORT!, 10);
|
||||||
const dexAddress = `${host}:${port}`;
|
const dexAddress = `${dexHost}:${dexPort}`;
|
||||||
hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress);
|
hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress);
|
||||||
await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml);
|
await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml);
|
||||||
|
|
||||||
const baseUrl = `http://${host}:${port}`;
|
const baseUrl = `http://${dexHost}:${dexPort}`;
|
||||||
return {
|
return {
|
||||||
host,
|
host: dexHost,
|
||||||
port,
|
port: dexPort,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
configDir: tempDir,
|
configDir: tempDir,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dexStart(): Promise<DexInstance> {
|
export async function dexStart(): Promise<DexInstance> {
|
||||||
const dexCfg = await produceConfigWithSynapseURLAdded();
|
const dexCfg = await produceConfigWithSynapseURLAdded();
|
||||||
console.log(`Starting dex with config dir ${dexCfg.configDir}...`);
|
console.log(`Starting dex with config dir ${dexCfg.configDir}...`);
|
||||||
const dexId = await dockerRun({
|
const dexId = await dockerRun({
|
||||||
@ -99,34 +98,11 @@ async function dexStart(): Promise<DexInstance> {
|
|||||||
return dex;
|
return dex;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dexStop(id: string): Promise<void> {
|
export async function dexStop(id: string): Promise<void> {
|
||||||
const dexCfg = dexConfigs.get(id);
|
const dexCfg = dexConfigs.get(id);
|
||||||
if (!dexCfg) throw new Error("Unknown dex ID");
|
if (!dexCfg) throw new Error("Unknown dex ID");
|
||||||
await dockerStop({ containerId: id, });
|
await dockerStop({ containerId: id, });
|
||||||
await fse.remove(dexCfg.configDir);
|
await fse.remove(dexCfg.configDir);
|
||||||
dexConfigs.delete(id);
|
dexConfigs.delete(id);
|
||||||
console.log(`Stopped dex id ${id}.`);
|
console.log(`Stopped dex id ${id}.`);
|
||||||
// cypress deliberately fails if you return 'undefined', so
|
|
||||||
// return null to signal all is well, and we've handled the task.
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
export function dexDocker(on: PluginEvents, config: PluginConfigOptions) {
|
|
||||||
env = config.env;
|
|
||||||
|
|
||||||
on("task", {
|
|
||||||
dexStart,
|
|
||||||
dexStop,
|
|
||||||
});
|
|
||||||
|
|
||||||
on("after:spec", async (spec) => {
|
|
||||||
for (const dexId of dexConfigs.keys()) {
|
|
||||||
console.warn(`Cleaning up dex ID ${dexId} after ${spec.name}`);
|
|
||||||
await dexStop(dexId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -20,11 +20,6 @@ import * as os from "os";
|
|||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
import * as fse from "fs-extra";
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
|
||||||
|
|
||||||
// A cypress plugin to run docker commands
|
|
||||||
|
|
||||||
export function dockerRun(args: {
|
export function dockerRun(args: {
|
||||||
image: string;
|
image: string;
|
||||||
containerName: string;
|
containerName: string;
|
||||||
@ -48,8 +43,7 @@ export function dockerRun(args: {
|
|||||||
...params,
|
...params,
|
||||||
args.image,
|
args.image,
|
||||||
... appParams
|
... appParams
|
||||||
], (err, stdout, stderr) => {
|
], (err, stdout) => {
|
||||||
console.log("error", err, "stdout", stdout, "stderr", stderr);
|
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
@ -155,18 +149,3 @@ export function dockerRm(args: {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
export function docker(on: PluginEvents, config: PluginConfigOptions) {
|
|
||||||
console.log("Code gets to here!");
|
|
||||||
on("task", {
|
|
||||||
dockerRun,
|
|
||||||
dockerExec,
|
|
||||||
dockerLogs,
|
|
||||||
dockerStop,
|
|
||||||
dockerRm,
|
|
||||||
dockerCreateNetwork
|
|
||||||
});
|
|
||||||
}
|
|
@ -21,9 +21,8 @@ import * as os from "os";
|
|||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as fse from "fs-extra";
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
|
||||||
import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
|
import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
|
||||||
|
import { request } from "@playwright/test";
|
||||||
|
|
||||||
|
|
||||||
// A cypress plugins to add command to start & stop synapses in
|
// A cypress plugins to add command to start & stop synapses in
|
||||||
@ -43,7 +42,6 @@ export interface SynapseInstance extends SynapseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const synapses = new Map<string, SynapseInstance>();
|
const synapses = new Map<string, SynapseInstance>();
|
||||||
let env;
|
|
||||||
|
|
||||||
function randB64Bytes(numBytes: number): string {
|
function randB64Bytes(numBytes: number): string {
|
||||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||||
@ -56,7 +54,7 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
|||||||
if (!stats?.isDirectory) {
|
if (!stats?.isDirectory) {
|
||||||
throw new Error(`No such template: ${template}`);
|
throw new Error(`No such template: ${template}`);
|
||||||
}
|
}
|
||||||
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-'));
|
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'synapsedocker-'));
|
||||||
|
|
||||||
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
|
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
|
||||||
console.log(`Copy ${templateDir} -> ${tempDir}`);
|
console.log(`Copy ${templateDir} -> ${tempDir}`);
|
||||||
@ -66,9 +64,10 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
|||||||
const macaroonSecret = randB64Bytes(16);
|
const macaroonSecret = randB64Bytes(16);
|
||||||
const formSecret = randB64Bytes(16);
|
const formSecret = randB64Bytes(16);
|
||||||
|
|
||||||
const host = env["SYNAPSE_IP_ADDRESS"];
|
const synapseHost = process.env["SYNAPSE_IP_ADDRESS"]!!;
|
||||||
const port = parseInt(env["SYNAPSE_PORT"], 10);
|
const synapsePort = parseInt(process.env["SYNAPSE_PORT"]!, 10);
|
||||||
const baseUrl = `http://${host}:${port}`;
|
const baseUrl = `http://${synapseHost}:${synapsePort}`;
|
||||||
|
|
||||||
|
|
||||||
// now copy homeserver.yaml, applying substitutions
|
// now copy homeserver.yaml, applying substitutions
|
||||||
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
|
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
|
||||||
@ -77,7 +76,10 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
|||||||
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
|
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
|
||||||
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
|
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
|
||||||
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
|
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
|
||||||
const dexUrl = `http://${env["DEX_IP_ADDRESS"]}:${env["DEX_PORT"]}/dex`;
|
|
||||||
|
const dexHost = process.env["DEX_IP_ADDRESS"];
|
||||||
|
const dexPort = process.env["DEX_PORT"];
|
||||||
|
const dexUrl = `http://${dexHost}:${dexPort}/dex`;
|
||||||
hsYaml = hsYaml.replace(/{{OIDC_ISSUER}}/g, dexUrl);
|
hsYaml = hsYaml.replace(/{{OIDC_ISSUER}}/g, dexUrl);
|
||||||
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
|
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
|
||||||
|
|
||||||
@ -89,8 +91,8 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
|||||||
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
|
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port,
|
port: synapsePort,
|
||||||
host,
|
host: synapseHost,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
configDir: tempDir,
|
configDir: tempDir,
|
||||||
registrationSecret,
|
registrationSecret,
|
||||||
@ -100,7 +102,7 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
|||||||
// Start a synapse instance: the template must be the name of
|
// Start a synapse instance: the template must be the name of
|
||||||
// one of the templates in the cypress/plugins/synapsedocker/templates
|
// one of the templates in the cypress/plugins/synapsedocker/templates
|
||||||
// directory
|
// directory
|
||||||
async function synapseStart(template: string): Promise<SynapseInstance> {
|
export async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
const synCfg = await cfgDirFromTemplate(template);
|
const synCfg = await cfgDirFromTemplate(template);
|
||||||
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||||
await dockerCreateNetwork({ networkName: "hydrogen" });
|
await dockerCreateNetwork({ networkName: "hydrogen" });
|
||||||
@ -143,12 +145,12 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
|||||||
return synapse;
|
return synapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function synapseStop(id: string): Promise<void> {
|
export async function synapseStop(id: string): Promise<void> {
|
||||||
const synCfg = synapses.get(id);
|
const synCfg = synapses.get(id);
|
||||||
|
|
||||||
if (!synCfg) throw new Error("Unknown synapse ID");
|
if (!synCfg) throw new Error("Unknown synapse ID");
|
||||||
|
|
||||||
const synapseLogsPath = path.join("cypress", "synapselogs", id);
|
const synapseLogsPath = path.join("playwright", "synapselogs", id);
|
||||||
await fse.ensureDir(synapseLogsPath);
|
await fse.ensureDir(synapseLogsPath);
|
||||||
|
|
||||||
await dockerLogs({
|
await dockerLogs({
|
||||||
@ -162,42 +164,40 @@ async function synapseStop(id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await fse.remove(synCfg.configDir);
|
await fse.remove(synCfg.configDir);
|
||||||
|
|
||||||
synapses.delete(id);
|
synapses.delete(id);
|
||||||
|
|
||||||
console.log(`Stopped synapse id ${id}.`);
|
console.log(`Stopped synapse id ${id}.`);
|
||||||
// cypress deliberately fails if you return 'undefined', so
|
|
||||||
// return null to signal all is well, and we've handled the task.
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) {
|
|
||||||
env = config.env;
|
|
||||||
|
|
||||||
|
|
||||||
on("task", {
|
interface Credentials {
|
||||||
synapseStart,
|
accessToken: string;
|
||||||
synapseStop,
|
userId: string;
|
||||||
});
|
deviceId: string;
|
||||||
|
homeServer: string;
|
||||||
|
}
|
||||||
|
|
||||||
on("after:spec", async (spec) => {
|
export async function registerUser(synapse: SynapseInstance, username: string, password: string, displayName?: string,): Promise<Credentials> {
|
||||||
// Cleans up any remaining synapse instances after a spec run
|
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
|
||||||
// This is on the theory that we should avoid re-using synapse
|
const context = await request.newContext({ baseURL: url });
|
||||||
// instances between spec runs: they should be cheap enough to
|
const { nonce } = await (await context.get(url)).json();
|
||||||
// start that we can have a separate one for each spec run or even
|
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
|
||||||
// test. If we accidentally re-use synapses, we could inadvertently
|
`${nonce}\0${username}\0${password}\0notadmin`,
|
||||||
// make our tests depend on each other.
|
).digest('hex');
|
||||||
for (const synId of synapses.keys()) {
|
const response = await (await context.post(url, {
|
||||||
console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`);
|
data: {
|
||||||
await synapseStop(synId);
|
nonce,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
mac,
|
||||||
|
admin: false,
|
||||||
|
displayname: displayName,
|
||||||
}
|
}
|
||||||
});
|
})).json();
|
||||||
|
return {
|
||||||
on("before:run", async () => {
|
homeServer: response.home_server,
|
||||||
// tidy up old synapse log files before each run
|
accessToken: response.access_token,
|
||||||
await fse.emptyDir(path.join("cypress", "synapselogs"));
|
userId: response.user_id,
|
||||||
});
|
deviceId: response.device_id,
|
||||||
|
};
|
||||||
}
|
}
|
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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import { test } from '@playwright/test';
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
test("App has no startup errors that prevent UI render", async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
import "./synapse";
|
await page.getByText('Log In', { exact: true }).waitFor();
|
||||||
import "./dex";
|
});
|
13
yarn.lock
13
yarn.lock
@ -114,6 +114,14 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@playwright/test@^1.27.1":
|
||||||
|
version "1.27.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.27.1.tgz#9364d1e02021261211c8ff586d903faa79ce95c4"
|
||||||
|
integrity sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
playwright-core "1.27.1"
|
||||||
|
|
||||||
"@trysound/sax@0.2.0":
|
"@trysound/sax@0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||||
@ -2040,6 +2048,11 @@ pify@^2.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
||||||
|
|
||||||
|
playwright-core@1.27.1:
|
||||||
|
version "1.27.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.27.1.tgz#840ef662e55a3ed759d8b5d3d00a5f885a7184f4"
|
||||||
|
integrity sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==
|
||||||
|
|
||||||
postcss-css-variables@^0.18.0:
|
postcss-css-variables@^0.18.0:
|
||||||
version "0.18.0"
|
version "0.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93"
|
resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93"
|
||||||
|
Loading…
Reference in New Issue
Block a user