mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
fixup name change
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
const sharedConfig = require("../shared/jest.config.angular");
|
||||
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
124
libs/playwright-helpers/src/acts/authenticate-as.ts
Normal file
124
libs/playwright-helpers/src/acts/authenticate-as.ts
Normal 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);
|
||||
}
|
||||
8
libs/playwright-helpers/src/acts/index.ts
Normal file
8
libs/playwright-helpers/src/acts/index.ts
Normal 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";
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./scene";
|
||||
export * from "./recipes";
|
||||
export * from "./acts";
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user