import { test, expect, Page, BrowserContext } from "@playwright/test"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import os from "os"; import fs from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const EXTENSION_PATH = join(__dirname, "..", "dist"); async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise(r => setTimeout(r, 500)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No SW after ${maxWait}ms`); return new URL(sw.url()).host; } async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> { const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(3000); return { popup, extId }; } async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } async function setupVault(ctx: BrowserContext, password = "password123") { const { popup } = await loadPopup(ctx); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill(password); await popup.locator("#confirm-password").fill(password); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup }; } test.describe("Password Generator", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("PG01 - generator button is visible", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator"); await expect(genBtn.first()).toBeVisible({ timeout: 2000 }); await ctx.close(); }); test("PG02 - generator opens on click", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(300); const generated = popup.locator("[class*='generated'], [class*='password']").first(); const hasGen = await generated.isVisible({ timeout: 2000 }).catch(() => false); expect(hasGen).toBeTruthy(); await ctx.close(); }); test("PG03 - generated password is not empty", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const password = popup.locator("[class*='generated-password'], [class*='password-output']").first(); const text = (await password.textContent().catch(() => "")) ?? ""; expect(text.length).toBeGreaterThan(0); await ctx.close(); }); test("PG04 - regenerate button creates new password", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const pass1 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; const regenBtn = popup.locator("text=regenerate, [aria-label*='regenerate' i]").first(); await regenBtn.click(); await popup.waitForTimeout(300); const pass2 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; expect(pass2).not.toEqual(pass1); await ctx.close(); }); test("PG05 - copy button copies password", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const copyBtn = popup.locator("[aria-label*='copy' i], [aria-label*='Copy']").first(); await copyBtn.click(); await popup.waitForTimeout(300); const copied = popup.locator("text=/copied|\\u2713/i"); const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false); expect(hasCopied).toBeTruthy(); await ctx.close(); }); test("PG06 - password length slider", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const lengthSlider = popup.locator("input[type='range'], [class*='length']").first(); const hasSlider = await lengthSlider.isVisible().catch(() => false); expect(hasSlider).toBeTruthy(); await ctx.close(); }); test("PG07 - uppercase toggle", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const uppercaseToggle = popup.locator("text=/uppercase|A-Z/i").first(); const hasToggle = await uppercaseToggle.isVisible().catch(() => false); expect(hasToggle).toBeTruthy(); await ctx.close(); }); test("PG08 - lowercase toggle", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const lowercaseToggle = popup.locator("text=/lowercase|a-z/i").first(); const hasToggle = await lowercaseToggle.isVisible().catch(() => false); expect(hasToggle).toBeTruthy(); await ctx.close(); }); test("PG09 - numbers toggle", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const numbersToggle = popup.locator("text=/numbers|0-9/i").first(); const hasToggle = await numbersToggle.isVisible().catch(() => false); expect(hasToggle).toBeTruthy(); await ctx.close(); }); test("PG10 - symbols toggle", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const symbolsToggle = popup.locator("text=/symbols|!@#$/i").first(); const hasToggle = await symbolsToggle.isVisible().catch(() => false); expect(hasToggle).toBeTruthy(); await ctx.close(); }); test("PG11 - generated password contains uppercase when enabled", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const uppercaseToggle = popup.locator("text=/uppercase|A-Z/i]").first(); const isEnabled = await uppercaseToggle.isChecked().catch(() => true); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; if (isEnabled) expect(password).toMatch(/[A-Z]/); await ctx.close(); }); test("PG12 - generated password contains lowercase when enabled", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; expect(password).toMatch(/[a-z]/); await ctx.close(); }); test("PG13 - generated password contains numbers when enabled", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const numbersToggle = popup.locator("text=/numbers|0-9/i").first(); const isEnabled = await numbersToggle.isChecked().catch(() => true); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; if (isEnabled) expect(password).toMatch(/[0-9]/); await ctx.close(); }); test("PG14 - generated password contains symbols when enabled", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const symbolsToggle = popup.locator("text=/symbols|!@#$/i").first(); const isEnabled = await symbolsToggle.isChecked().catch(() => true); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; if (isEnabled) expect(password).toMatch(/[!@#$%^&*()_+\-=\[\]{}|;':\",.\/<>?]/); await ctx.close(); }); test("PG15 - password strength indicator", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const strength = popup.locator("[class*='strength'], [class*='weak'], [class*='strong']").first(); const hasStrength = await strength.isVisible().catch(() => false); expect(hasStrength).toBeTruthy(); await ctx.close(); }); test("PG16 - generator has keyboard shortcut", async () => { const { popup } = await setupVault(ctx); await popup.keyboard.press("Control+Shift+G"); await popup.waitForTimeout(300); const gen = popup.locator("[class*='generator'], [class*='password']").first(); const hasGen = await gen.isVisible({ timeout: 2000 }).catch(() => false); expect(hasGen).toBeTruthy(); await ctx.close(); }); test("PG17 - use password button", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const useBtn = popup.locator("text=/use|apply/i").first(); await useBtn.click(); await popup.waitForTimeout(300); await ctx.close(); }); test("PG18 - generator closes on escape", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(300); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); await ctx.close(); }); test("PG19 - minimum length is enforced", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; expect(password.length).toBeGreaterThanOrEqual(8); await ctx.close(); }); test("PG20 - maximum length is reasonable", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; expect(password.length).toBeLessThanOrEqual(128); await ctx.close(); }); }); test.describe("Password Generator - Advanced", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("PGA01 - passphrase generation option", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const passphraseOpt = popup.locator("text=/passphrase|words/i").first(); const hasPassphrase = await passphraseOpt.isVisible().catch(() => false); expect(hasPassphrase).toBeTruthy(); await ctx.close(); }); test("PGA02 - word count for passphrases", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const passphraseOpt = popup.locator("text=/passphrase|words/i").first(); const hasOpt = await passphraseOpt.isVisible().catch(() => false); if (hasOpt) { await passphraseOpt.click(); await popup.waitForTimeout(300); } const wordCount = popup.locator("input[type='number'], [class*='word']").first(); const hasWordCount = await wordCount.isVisible().catch(() => false); expect(hasWordCount).toBeTruthy(); await ctx.close(); }); test("PGA03 - exclude ambiguous characters option", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const excludeOpt = popup.locator("text=/exclude|ambiguous/i").first(); const hasExclude = await excludeOpt.isVisible().catch(() => false); expect(hasExclude).toBeTruthy(); await ctx.close(); }); test("PGA04 - history of generated passwords", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); await popup.locator("text=regenerate, [aria-label*='regenerate' i]").first().click(); await popup.waitForTimeout(300); const history = popup.locator("text=/history|recent/i").first(); const hasHistory = await history.isVisible().catch(() => false); if (hasHistory) expect(true).toBeTruthy(); else expect(hasHistory).toBeFalsy(); await ctx.close(); }); test("PGA05 - custom character exclusions", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const customExclude = popup.locator("text=/custom|exclude/i").first(); const hasCustom = await customExclude.isVisible().catch(() => false); expect(hasCustom).toBeTruthy(); await ctx.close(); }); test("PGA06 - pronounceable password option", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const pronounceable = popup.locator("text=/pronounceable|readable/i").first(); const hasPronounceable = await pronounceable.isVisible().catch(() => false); expect(hasPronounceable).toBeTruthy(); await ctx.close(); }); test("PGA07 - pin-only generation option", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const pinOpt = popup.locator("text=/pin|digit/i").first(); const hasPin = await pinOpt.isVisible().catch(() => false); expect(hasPin).toBeTruthy(); await ctx.close(); }); test("PGA08 - generated password entropy is sufficient", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; expect(password.length).toBeGreaterThanOrEqual(12); await ctx.close(); }); }); test.describe("Password Generator - Entry Integration", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("PGI01 - generator works in new entry form", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.waitForTimeout(300); const genBtn = popup.locator("[aria-label*='generate' i]").first(); await genBtn.click(); await popup.waitForTimeout(500); const generated = popup.locator("[class*='generated'], [class*='password']").first(); const hasGen = await generated.isVisible({ timeout: 2000 }).catch(() => false); expect(hasGen).toBeTruthy(); await ctx.close(); }); test("PGI02 - generated password fills entry password field", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.waitForTimeout(300); const genBtn = popup.locator("[aria-label*='generate' i]").first(); await genBtn.click(); await popup.waitForTimeout(500); const useBtn = popup.locator("text=/use|apply/i").first(); await useBtn.click(); await popup.waitForTimeout(300); const passwordField = popup.locator(".peach-new-field-input[placeholder='password']"); const hasPassword = await passwordField.inputValue().catch(() => "") ?? ""; expect(hasPassword.length).toBeGreaterThan(0); await ctx.close(); }); test("PGI03 - generator accessible from entry detail", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("GenTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://gentest.com"); await popup.locator('input[placeholder="username or email"]').fill("gentest@user.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const genBtn = popup.locator("[aria-label*='generate' i]").first(); const hasGen = await genBtn.isVisible().catch(() => false); expect(hasGen).toBeTruthy(); await ctx.close(); }); test("PGI04 - regenerate updates entry password", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.waitForTimeout(300); const genBtn = popup.locator("[aria-label*='generate' i]").first(); await genBtn.click(); await popup.waitForTimeout(500); const pass1 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; const regenBtn = popup.locator("text=regenerate").first(); await regenBtn.click(); await popup.waitForTimeout(300); const pass2 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? ""; expect(pass1).not.toEqual(pass2); await ctx.close(); }); test("PGI05 - password copied notification", async () => { const { popup } = await setupVault(ctx); const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first(); await genBtn.click(); await popup.waitForTimeout(500); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(300); const notification = popup.locator("text=/copied|\\u2713|copied to clipboard/i"); const hasNotification = await notification.isVisible({ timeout: 2000 }).catch(() => false); expect(hasNotification).toBeTruthy(); await ctx.close(); }); });