1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

fixup name change

This commit is contained in:
Matt Gibson
2025-10-10 10:15:10 -07:00
parent c57f439161
commit 1ab383b1ae
19 changed files with 157 additions and 6 deletions

2
.gitignore vendored
View File

@@ -63,3 +63,5 @@ apps/**/config/local.json
/blob-report/
/playwright/.cache/
/playwright/.auth/
# Stores local state data for playwright while it runs
/playwright-data/

View File

@@ -1,4 +1,4 @@
const sharedConfig = require("../../libs/shared/jest.config.angular");
const sharedConfig = require("../shared/jest.config.angular");
module.exports = {
...sharedConfig,

View File

@@ -0,0 +1,124 @@
import * as fs from "fs";
import { Page } from "@playwright/test";
import { webServerBaseUrl } from "@playwright-config";
import { Play, Scene } from "@bitwarden/playwright-helpers";
const hostname = new URL(webServerBaseUrl).hostname;
const dataDir = process.env.PLAYWRIGHT_DATA_DIR ?? "playwright-data";
// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
type AuthedUserData = {
email: string;
password: string;
scene: Scene;
};
type AuthenticatedContext = {
page: Page;
scene: Scene;
};
/**
* A map of already authenticated emails to their scenes.
*/
const AuthenticatedEmails = new Map<email, AuthedUserData>();
function dataFilePath(email: string): string {
return `${dataDir}/auth-${email}.json`;
}
function sessionFilePath(email: string): string {
return `${dataDir}/session-${email}.json`;
}
/**
* Helper to ensure a user exists and is authenticated in playwright tests.
*/
export async function authenticateAs(
page: Page,
email: string,
password: string,
): Promise<AuthenticatedContext> {
// Return existing scene if already authenticated
if (AuthenticatedEmails.has(email)) {
if (AuthenticatedEmails.get(email)!.password !== password) {
throw new Error(
`Email ${email} is already authenticated with a different password (${AuthenticatedEmails.get(email)!.password})`,
);
}
await page.context().storageState({ path: dataFilePath(email) });
await loadSession(page, email);
return {
page,
scene: AuthenticatedEmails.get(email)!.scene,
};
}
return newAuthenticateAs(email, password);
}
function newAuthenticateAs(email: string, password: string): Promise<AuthenticatedContext> {
using scene = await Play.scene(new SingleUserRecipe({ email, password }), {
downAfterAll: true,
});
await page.goto("/#/login");
await page.locator("#login_input_email").fill(scene.mangle("test@example.com"));
await page.locator("#login_button_continue").click();
await page.locator("#login_input_password").fill("asdfasdfasdf");
await page.locator("#login_button_submit").click();
await expect(page.getByRole("button", { name: "Add it later" })).toBeVisible();
await page.getByRole("button", { name: "Add it later" }).click();
await expect(page.locator("bit-simple-dialog")).toContainText(
"You can't autofill passwords without the browser extension",
);
await page.getByRole("link", { name: "Skip to web app" }).click();
await expect(page.locator("app-vault")).toContainText("There are no items to list. New item");
// Store the scene for future use
AuthenticatedEmails.set(email, { email, password, scene });
// Save storage state to avoid logging in again
await page.context().storageState({ path: dataFilePath(email) });
await saveSession(page, email);
return { page, scene };
}
async function saveSession(page: Page, email: string): Promise<void> {
// Get session storage and store as env variable
const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
fs.writeFileSync("playwright/.auth/session.json", sessionStorage, "utf-8");
// Set session storage in a new context
const sessionStorage = JSON.parse(fs.readFileSync("playwright/.auth/session.json", "utf-8"));
await context.addInitScript((storage) => {
if (window.location.hostname === "example.com") {
for (const [key, value] of Object.entries(storage)) {
window.sessionStorage.setItem(key, value);
}
}
}, sessionStorage);
}
async function loadSession(page: Page, email: string): Promise<void> {
if (!fs.existsSync(sessionFilePath(email))) {
throw new Error("No session file found");
}
// Set session storage in a new context
const sessionStorage = JSON.parse(fs.readFileSync(sessionFilePath(email), "utf-8"));
await context.addInitScript((storage) => {
if (window.location.hostname === hostname) {
for (const [key, value] of Object.entries(storage)) {
window.sessionStorage.setItem(key, value);
}
}
}, sessionStorage);
}

View File

@@ -0,0 +1,8 @@
/**
* Acts are Playwright setups that are intended to allow reuse across different tests.
* They should have logic to ensure they are run only once per unique input.
* They should also handle teardown of any resources they create.
* Finally, they should return any data needed to interact with the setup.
*/
export * from "./authenticate-as";

View File

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

View File

@@ -1,2 +1,6 @@
/**
* Recipes represent server-side seed recipes to create data for tests.
*/
export * from "./organization-with-users.recipe";
export * from "./single-user.recipe";

View File

@@ -8,7 +8,17 @@ import { Recipe } from "./recipes/recipe";
// First seed points at the seeder API proxy, second is the seed path of the SeedController
const seedApiUrl = new URL("/seed/seed/", webServerBaseUrl).toString();
class Scene implements UsingRequired {
/**
* A Scene contains logic to set up and tear down data for a test on the server.
* It is created by running a Recipe, which contains the arguments the server requires to create the data.
*
* Scenes are `Disposable`, meaning they must be used with the `using` keyword and will be automatically torn down when disposed.
* Options exist to modify this behavior.
*
* - {@link SceneOptions.noDown}: Useful for setting up data then using codegen to create tests that use the data. Remember to tear down the data manually.
* - {@link SceneOptions.downAfterAll}: Useful for expensive setups that you want to share across all tests in a worker or for writing acts.
*/
export class Scene implements UsingRequired {
private inited = false;
private _recipe?: Recipe<unknown>;
private mangledMap = new Map<string, string>();
@@ -125,7 +135,7 @@ export class Play {
*
* Example usage:
* ```ts
* import { Play, SingleUserRecipe } from "@bitwarden/playwright-scenes";
* import { Play, SingleUserRecipe } from "@bitwarden/playwright-helpers";
*
* test("my test", async ({ page }) => {
* using scene = await Play.scene(new SingleUserRecipe({ email: "
@@ -140,9 +150,10 @@ export class Play {
recipe: T,
options: SceneOptions = {},
): Promise<Scene> {
const scene = new Scene({ SCENE_OPTIONS_DEFAULTS, ...options });
const opts = { ...SCENE_OPTIONS_DEFAULTS, ...options };
const scene = new Scene(opts);
await scene.init(recipe);
if (!scene.options.noDown) {
if (!opts.noDown) {
seedIdsToTearDown.add(scene.seedId);
}
return scene;

View File

@@ -65,7 +65,8 @@
"@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"],
"@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/web-vault/*": ["./apps/web/src/*"],
"@playwright-config": ["./playwright.config.ts"]
"@playwright-config": ["./playwright.config.ts"],
"@playwright-data": ["./playwright-data"]
},
"plugins": [
{