1234567891011import { expect, type BrowserContext, type Page } from "@playwright/test";12import { fileURLToPath } from "url";13import { dirname, join } from "path";14import os from "os";15import fs from "fs/promises";1617const __filename = fileURLToPath(import.meta.url);18const __dirname = dirname(__filename);19export const EXTENSION_PATH = join(__dirname, "..", "..", "dist");202122export const VAULT_PASSWORD = "correct horse battery staple";232425export const UNLOCKED = ".peach-lock-btn[aria-label='Lock vault']";2627export async function makeContext(prefix = "peach"): Promise<BrowserContext> {28 const { chromium } = await import("playwright");29 const userDataDir = join(30 os.tmpdir(),31 `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,32 );33 await fs.mkdir(userDataDir, { recursive: true });34 return chromium.launchPersistentContext(userDataDir, {35 headless: false,36 args: [37 `--load-extension=${EXTENSION_PATH}`,38 `--disable-extensions-except=${EXTENSION_PATH}`,39 ],40 });41}4243export async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {44 const start = Date.now();45 let sw = ctx.serviceWorkers()[0];46 while (!sw && Date.now() - start < maxWait) {47 await new Promise((r) => setTimeout(r, 500));48 sw = ctx.serviceWorkers()[0];49 }50 if (!sw) throw new Error(`No SW after ${maxWait}ms`);51 return new URL(sw.url()).host;52}5354export async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page; extId: string }> {55 const extId = await waitForSW(ctx);56 const popup = await ctx.newPage();57 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);58 await popup.waitForTimeout(3000);59 return { popup, extId };60}616263async function reachCreateScreen(popup: Page): Promise<void> {64 65 66 const advanceButton = popup.getByRole("button", { name: /next|get started|create vault/i });67 68 69 const pw = popup.locator("#master-password");70 for (let i = 0; i < 5 && !(await pw.isVisible().catch(() => false)); i++) {71 if (await advanceButton.isVisible({ timeout: 500 }).catch(() => false)) {72 await advanceButton.click();73 await popup.waitForTimeout(300);74 continue;75 }76 await popup.locator(".peach-onboarding-card").click();77 await popup.waitForTimeout(300);78 }79 if (!(await pw.isVisible().catch(() => false))) {80 throw new Error("setupVault: never reached the create-vault screen (#master-password)");81 }82}8384858687888990export async function setupVault(91 ctx: BrowserContext,92 password = VAULT_PASSWORD,93 opts: { confirmRecovery?: boolean } = {},94): Promise<{ popup: Page; extId: string }> {95 const { confirmRecovery = true } = opts;96 const { popup, extId } = await loadPopup(ctx);97 await reachCreateScreen(popup);98 await popup.locator("#master-password").fill(password);99 await popup.locator("#confirm-password").fill(password);100 101 102 103 const createBtn = popup.locator(".peach-newvault-submit");104 if (!(await createBtn.isEnabled().catch(() => false))) {105 await expect(createBtn, `create-vault gate not met: "${await createBtn.textContent()}"`)106 .toBeEnabled({ timeout: 5000 });107 }108 await createBtn.click();109 110 111 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 30000 });112 if (confirmRecovery) await completeRecoveryConfirmation(popup);113 return { popup, extId };114}115116export async function completeRecoveryConfirmation(popup: Page): Promise<void> {117 const recoveryWords = popup.locator(".peach-recovery-word-text");118 const confirmInputs = popup.locator(".peach-recovery-confirm-input");119 const confirmButton = popup.getByRole("button", { name: /i wrote them down/i });120121 await expect(confirmInputs).toHaveCount(3);122 123 124 await expect(confirmInputs.first()).toBeEnabled({ timeout: 30000 });125126 const words = await recoveryWords.allTextContents();127 const confirmedWords = [words[2], words[6], words[10]];128 for (let i = 0; i < confirmedWords.length; i++) {129 const word = confirmedWords[i]?.trim();130 if (!word) throw new Error(`missing recovery word at position ${i}`);131 await confirmInputs.nth(i).fill(word);132 }133134 await expect(confirmButton).toBeEnabled({ timeout: 2000 });135 await confirmButton.click();136 await expect(137 popup.locator(UNLOCKED),138 "setupVault: recovery confirmation did not land in the unlocked vault",139 ).toBeVisible({ timeout: 10000 });140}141142export async function lockVault(popup: Page): Promise<void> {143 const lockBtn = popup.locator(UNLOCKED);144 await expect(lockBtn).toBeVisible({ timeout: 15000 });145 await lockBtn.click();146 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 });147}148149export async function unlockVault(popup: Page, password = VAULT_PASSWORD): Promise<void> {150 await popup.locator("#unlock-password").fill(password);151 await popup.getByRole("button", { name: /unlock/i }).click();152 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 20000 });153}154155export async function openSettings(popup: Page): Promise<void> {156 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();157 await expect(popup.locator(".stk-shell")).toBeVisible({ timeout: 10000 });158}159160161162163164165export async function openList(popup: Page): Promise<void> {166 if (await popup.locator(".peach-overlay-view .peach-search-input").isVisible().catch(() => false)) return;167 await popup.locator(".peach-search-wrap .peach-search-input").click();168 await expect(popup.locator(".peach-overlay-view .peach-search-input")).toBeVisible({ timeout: 5000 });169}170171172export function listSearch(popup: Page) {173 return popup.locator(".peach-overlay-view .peach-search-input");174}175176177178179180181export async function createEntryViaForm(182 popup: Page,183 entry: { title: string; url?: string; username?: string; password?: string; totp?: string; notes?: string },184): Promise<void> {185 await openList(popup);186 await popup.locator(".peach-new-entry-row").click();187 await popup.locator(".peach-new-title-input").fill(entry.title);188 if (entry.url) await popup.locator(".peach-domain-value--edit").fill(entry.url);189 if (entry.username) await popup.locator('input[placeholder="johndoe"]').fill(entry.username);190 if (entry.password) await popup.locator('input[placeholder="password"]').fill(entry.password);191 192 if (entry.totp) {193 await popup.getByRole("button", { name: /add 2FA/i }).click();194 await popup.locator('input[placeholder*="BASE32" i]').fill(entry.totp);195 }196 if (entry.notes) {197 await popup.getByRole("button", { name: /add notes/i }).click();198 await popup.locator(".peach-new-textarea").fill(entry.notes);199 }200 const create = popup.getByRole("button", { name: /^create$/i });201 await expect(create, "create button stayed disabled — the entry form rejected the input").toBeEnabled();202 await create.click();203 await expect(popup.locator(".peach-new-title-input")).toHaveCount(0, { timeout: 10000 });204}205206207export async function addEntry(208 popup: Page,209 entry: { title: string; url?: string; username?: string; password?: string; totp?: string },210): Promise<void> {211 const result = await popup.evaluate(async (payload) => {212 return new Promise<{ ok?: boolean; error?: string }>((resolve) => {213 chrome.runtime.sendMessage({ type: "ADD_ENTRY", payload: { type: "login", ...payload } }, resolve);214 });215 }, entry);216 217 218 if (!result?.ok) throw new Error(`addEntry(${entry.title}) failed: ${result?.error ?? "no response"}`);219}