1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-30 16:23:53 +00:00

Create playwright scenes framework

This is a client-side implementation of the db recipes seeding framework for bitwarden server
This commit is contained in:
Matt Gibson
2025-10-07 17:00:49 -07:00
parent b51002a345
commit 5c19a7c9dd
18 changed files with 253 additions and 0 deletions

1
.github/CODEOWNERS vendored
View File

@@ -219,3 +219,4 @@ apps/web/src/locales/en/messages.json
**/jest.config.js @bitwarden/team-platform-dev
**/project.jsons @bitwarden/team-platform-dev
libs/pricing @bitwarden/team-billing-dev
libs/playwright-scenes @bitwarden/team-architecture

View File

@@ -59,6 +59,7 @@ module.exports = {
"<rootDir>/libs/tools/send/send-ui/jest.config.js",
"<rootDir>/libs/user-core/jest.config.js",
"<rootDir>/libs/vault/jest.config.js",
"<rootDir>/libs/playwright-scenes/jest.config.js",
],
// Workaround for a memory leak that crashes tests in CI:

View File

@@ -0,0 +1,5 @@
# playwright-scenes
Owned by: architecture
Framework for writing end-to-end playwright tests

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,13 @@
const sharedConfig = require("../../libs/shared/jest.config.angular");
module.exports = {
...sharedConfig,
displayName: "playwright-scenes",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/playwright-scenes",
};

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/playwright-scenes",
"version": "0.0.1",
"description": "Framework for writing end-to-end playwright tests",
"private": true,
"type": "commonjs",
"main": "index.js",
"types": "index.d.ts",
"license": "GPL-3.0",
"author": "platform"
}

View File

@@ -0,0 +1,40 @@
{
"name": "playwright-scenes",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/playwright-scenes/src",
"projectType": "library",
"tags": ["!dependsOn:common"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/playwright-scenes",
"main": "libs/playwright-scenes/src/index.ts",
"tsConfig": "libs/playwright-scenes/tsconfig.lib.json",
"assets": ["libs/playwright-scenes/*.md"],
"rootDir": "libs/playwright-scenes/src"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/playwright-scenes/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/playwright-scenes/jest.config.js"
}
},
"e2e": {
"executor": "nx:run-commands",
"options": {
"command": "npx playwright test"
}
}
}
}

View File

@@ -0,0 +1,2 @@
export * from "./scene";
export * from "./recipes";

View File

@@ -0,0 +1 @@
export * from "./organization-with-users.recipe";

View File

@@ -0,0 +1,9 @@
import { Recipe } from "./recipe";
export class OrganizationWithUsersRecipe extends Recipe<{
name: string;
numUsers: number;
domain: string;
}> {
template: string = "OrganizationWithUsersRecipe";
}

View File

@@ -0,0 +1,52 @@
import * as playwrightConfig from "../../../../playwright.config";
const { webServer } = playwrightConfig.default as { webServer: { url: string } };
export abstract class Recipe<TUp> {
abstract template: string;
private seedId?: string;
constructor(private upArgs: TUp) {}
async up(): Promise<Record<string, string>> {
const response = await fetch(`${webServer.url}/api/seed`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
template: this.template,
arguments: this.upArgs,
}),
});
if (!response.ok) {
throw new Error(`Failed to seed recipe: ${response.statusText}`);
}
const result = JSON.parse(await response.json()) as SeedResult;
this.seedId = result.seedId;
return result.mangleMap;
}
async down(): Promise<void> {
const response = await fetch(`${webServer.url}/api/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
template: this.template,
seedId: this.seedId,
}),
});
if (!response.ok) {
throw new Error(`Failed to delete recipe: ${response.statusText}`);
}
}
}
export interface SeedResult {
mangleMap: Record<string, string>;
seedId: string;
}

View File

@@ -0,0 +1,66 @@
import { expect } from "@playwright/test";
import { UsingRequired } from "@bitwarden/common/platform/misc/using-required";
import { OrganizationWithUsersRecipe } from "./recipes/organization-with-users.recipe";
import { Recipe } from "./recipes/recipe";
export class Scene implements UsingRequired {
private inited = false;
private recipe?: Recipe<unknown>;
private mangledMap = new Map<string, string>();
[Symbol.dispose] = () => {
if (!this.inited) {
return;
}
if (!this.recipe) {
throw new Error("Scene was not properly initialized");
}
// Fire off an unawaited promise to delete the side effects of the scene
void this.recipe.down();
};
mangle(id: string): string {
if (!this.inited) {
throw new Error("Scene must be initialized before mangling ids");
}
return this.mangledMap.get(id) ?? id;
}
async init<T extends Recipe<TUp>, TUp>(recipe: T): Promise<void> {
if (this.inited) {
throw new Error("Scene has already been initialized");
}
this.recipe = recipe;
this.inited = true;
const response = await recipe.up();
this.mangledMap = new Map(Object.entries(response.mangleMap));
}
}
export class Play {
static async scene<T extends Recipe<TUp>, TUp>(recipe: T): Promise<Scene> {
const scene = new Scene();
await scene.init(recipe);
return scene;
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- example usage of the framework
async function test() {
// example usage
const recipe = new OrganizationWithUsersRecipe({
name: "My Org",
numUsers: 3,
domain: "example.com",
});
using scene = await Play.scene(recipe);
expect(scene.mangle("my-id")).toBe("my-id");
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

9
package-lock.json generated
View File

@@ -395,6 +395,11 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"libs/playwright-scenes": {
"name": "@bitwarden/playwright-scenes",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/pricing": {
"name": "@bitwarden/pricing",
"version": "0.0.0",
@@ -4685,6 +4690,10 @@
"resolved": "libs/platform",
"link": true
},
"node_modules/@bitwarden/playwright-scenes": {
"resolved": "libs/playwright-scenes",
"link": true
},
"node_modules/@bitwarden/pricing": {
"resolved": "libs/pricing",
"link": true

View File

@@ -49,6 +49,7 @@
"@bitwarden/nx-plugin": ["libs/nx-plugin/src/index.ts"],
"@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/platform/*": ["./libs/platform/src/*"],
"@bitwarden/playwright-scenes": ["libs/playwright-scenes/src/index.ts"],
"@bitwarden/pricing": ["libs/pricing/src/index.ts"],
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
"@bitwarden/serialization": ["libs/serialization/src/index.ts"],