1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");1011async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {12 const start = Date.now();13 let sw = ctx.serviceWorkers()[0];14 while (!sw && Date.now() - start < maxWait) {15 await new Promise(r => setTimeout(r, 500));16 sw = ctx.serviceWorkers()[0];17 }18 if (!sw) throw new Error(`No SW after ${maxWait}ms`);19 return new URL(sw.url()).host;20}2122async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> {23 const extId = await waitForSW(ctx);24 const popup = await ctx.newPage();25 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);26 await popup.waitForTimeout(3000);27 return { popup, extId };28}2930async function makeContext() {31 const { chromium } = await import("playwright");32 const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`);33 await fs.mkdir(userDataDir, { recursive: true });34 return chromium.launchPersistentContext(userDataDir, {35 headless: false,36 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],37 });38}3940async function setupVault(ctx: BrowserContext, password = "password123") {41 const { popup } = await loadPopup(ctx);42 const pw = popup.locator("#master-password");43 if (await pw.isVisible()) {44 await pw.fill(password);45 await popup.locator("#confirm-password").fill(password);46 await popup.getByRole("button", { name: /create vault/i }).click();47 await popup.waitForTimeout(2000);48 }49 return { popup };50}5152test.describe("Password Generator", () => {53 let ctx: BrowserContext;54 test.afterEach(async () => { await ctx.close(); });55 test.beforeEach(async () => { ctx = await makeContext(); });5657 test("PG01 - generator button is visible", async () => {58 const { popup } = await setupVault(ctx);59 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator");60 await expect(genBtn.first()).toBeVisible({ timeout: 2000 });61 await ctx.close();62 });6364 test("PG02 - generator opens on click", async () => {65 const { popup } = await setupVault(ctx);66 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();67 await genBtn.click();68 await popup.waitForTimeout(300);69 const generated = popup.locator("[class*='generated'], [class*='password']").first();70 const hasGen = await generated.isVisible({ timeout: 2000 }).catch(() => false);71 expect(hasGen).toBeTruthy();72 await ctx.close();73 });7475 test("PG03 - generated password is not empty", async () => {76 const { popup } = await setupVault(ctx);77 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();78 await genBtn.click();79 await popup.waitForTimeout(500);80 const password = popup.locator("[class*='generated-password'], [class*='password-output']").first();81 const text = (await password.textContent().catch(() => "")) ?? "";82 expect(text.length).toBeGreaterThan(0);83 await ctx.close();84 });8586 test("PG04 - regenerate button creates new password", async () => {87 const { popup } = await setupVault(ctx);88 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();89 await genBtn.click();90 await popup.waitForTimeout(500);91 const pass1 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";92 const regenBtn = popup.locator("text=regenerate, [aria-label*='regenerate' i]").first();93 await regenBtn.click();94 await popup.waitForTimeout(300);95 const pass2 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";96 expect(pass2).not.toEqual(pass1);97 await ctx.close();98 });99100 test("PG05 - copy button copies password", async () => {101 const { popup } = await setupVault(ctx);102 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();103 await genBtn.click();104 await popup.waitForTimeout(500);105 const copyBtn = popup.locator("[aria-label*='copy' i], [aria-label*='Copy']").first();106 await copyBtn.click();107 await popup.waitForTimeout(300);108 const copied = popup.locator("text=/copied|\\u2713/i");109 const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false);110 expect(hasCopied).toBeTruthy();111 await ctx.close();112 });113114 test("PG06 - password length slider", async () => {115 const { popup } = await setupVault(ctx);116 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();117 await genBtn.click();118 await popup.waitForTimeout(500);119 const lengthSlider = popup.locator("input[type='range'], [class*='length']").first();120 const hasSlider = await lengthSlider.isVisible().catch(() => false);121 expect(hasSlider).toBeTruthy();122 await ctx.close();123 });124125 test("PG07 - uppercase toggle", async () => {126 const { popup } = await setupVault(ctx);127 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();128 await genBtn.click();129 await popup.waitForTimeout(500);130 const uppercaseToggle = popup.locator("text=/uppercase|A-Z/i").first();131 const hasToggle = await uppercaseToggle.isVisible().catch(() => false);132 expect(hasToggle).toBeTruthy();133 await ctx.close();134 });135136 test("PG08 - lowercase toggle", async () => {137 const { popup } = await setupVault(ctx);138 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();139 await genBtn.click();140 await popup.waitForTimeout(500);141 const lowercaseToggle = popup.locator("text=/lowercase|a-z/i").first();142 const hasToggle = await lowercaseToggle.isVisible().catch(() => false);143 expect(hasToggle).toBeTruthy();144 await ctx.close();145 });146147 test("PG09 - numbers toggle", async () => {148 const { popup } = await setupVault(ctx);149 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();150 await genBtn.click();151 await popup.waitForTimeout(500);152 const numbersToggle = popup.locator("text=/numbers|0-9/i").first();153 const hasToggle = await numbersToggle.isVisible().catch(() => false);154 expect(hasToggle).toBeTruthy();155 await ctx.close();156 });157158 test("PG10 - symbols toggle", async () => {159 const { popup } = await setupVault(ctx);160 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();161 await genBtn.click();162 await popup.waitForTimeout(500);163 const symbolsToggle = popup.locator("text=/symbols|!@#$/i").first();164 const hasToggle = await symbolsToggle.isVisible().catch(() => false);165 expect(hasToggle).toBeTruthy();166 await ctx.close();167 });168169 test("PG11 - generated password contains uppercase when enabled", async () => {170 const { popup } = await setupVault(ctx);171 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();172 await genBtn.click();173 await popup.waitForTimeout(500);174 const uppercaseToggle = popup.locator("text=/uppercase|A-Z/i]").first();175 const isEnabled = await uppercaseToggle.isChecked().catch(() => true);176 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";177 if (isEnabled) expect(password).toMatch(/[A-Z]/);178 await ctx.close();179 });180181 test("PG12 - generated password contains lowercase when enabled", async () => {182 const { popup } = await setupVault(ctx);183 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();184 await genBtn.click();185 await popup.waitForTimeout(500);186 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";187 expect(password).toMatch(/[a-z]/);188 await ctx.close();189 });190191 test("PG13 - generated password contains numbers when enabled", async () => {192 const { popup } = await setupVault(ctx);193 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();194 await genBtn.click();195 await popup.waitForTimeout(500);196 const numbersToggle = popup.locator("text=/numbers|0-9/i").first();197 const isEnabled = await numbersToggle.isChecked().catch(() => true);198 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";199 if (isEnabled) expect(password).toMatch(/[0-9]/);200 await ctx.close();201 });202203 test("PG14 - generated password contains symbols when enabled", async () => {204 const { popup } = await setupVault(ctx);205 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();206 await genBtn.click();207 await popup.waitForTimeout(500);208 const symbolsToggle = popup.locator("text=/symbols|!@#$/i").first();209 const isEnabled = await symbolsToggle.isChecked().catch(() => true);210 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";211 if (isEnabled) expect(password).toMatch(/[!@#$%^&*()_+\-=\[\]{}|;':\",.\/<>?]/);212 await ctx.close();213 });214215 test("PG15 - password strength indicator", async () => {216 const { popup } = await setupVault(ctx);217 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();218 await genBtn.click();219 await popup.waitForTimeout(500);220 const strength = popup.locator("[class*='strength'], [class*='weak'], [class*='strong']").first();221 const hasStrength = await strength.isVisible().catch(() => false);222 expect(hasStrength).toBeTruthy();223 await ctx.close();224 });225226 test("PG16 - generator has keyboard shortcut", async () => {227 const { popup } = await setupVault(ctx);228 await popup.keyboard.press("Control+Shift+G");229 await popup.waitForTimeout(300);230 const gen = popup.locator("[class*='generator'], [class*='password']").first();231 const hasGen = await gen.isVisible({ timeout: 2000 }).catch(() => false);232 expect(hasGen).toBeTruthy();233 await ctx.close();234 });235236 test("PG17 - use password button", async () => {237 const { popup } = await setupVault(ctx);238 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();239 await genBtn.click();240 await popup.waitForTimeout(500);241 const useBtn = popup.locator("text=/use|apply/i").first();242 await useBtn.click();243 await popup.waitForTimeout(300);244 await ctx.close();245 });246247 test("PG18 - generator closes on escape", async () => {248 const { popup } = await setupVault(ctx);249 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();250 await genBtn.click();251 await popup.waitForTimeout(300);252 await popup.keyboard.press("Escape");253 await popup.waitForTimeout(200);254 await ctx.close();255 });256257 test("PG19 - minimum length is enforced", async () => {258 const { popup } = await setupVault(ctx);259 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();260 await genBtn.click();261 await popup.waitForTimeout(500);262 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";263 expect(password.length).toBeGreaterThanOrEqual(8);264 await ctx.close();265 });266267 test("PG20 - maximum length is reasonable", async () => {268 const { popup } = await setupVault(ctx);269 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();270 await genBtn.click();271 await popup.waitForTimeout(500);272 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";273 expect(password.length).toBeLessThanOrEqual(128);274 await ctx.close();275 });276});277278test.describe("Password Generator - Advanced", () => {279 let ctx: BrowserContext;280 test.afterEach(async () => { await ctx.close(); });281 test.beforeEach(async () => { ctx = await makeContext(); });282283 test("PGA01 - passphrase generation option", async () => {284 const { popup } = await setupVault(ctx);285 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();286 await genBtn.click();287 await popup.waitForTimeout(500);288 const passphraseOpt = popup.locator("text=/passphrase|words/i").first();289 const hasPassphrase = await passphraseOpt.isVisible().catch(() => false);290 expect(hasPassphrase).toBeTruthy();291 await ctx.close();292 });293294 test("PGA02 - word count for passphrases", async () => {295 const { popup } = await setupVault(ctx);296 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();297 await genBtn.click();298 await popup.waitForTimeout(500);299 const passphraseOpt = popup.locator("text=/passphrase|words/i").first();300 const hasOpt = await passphraseOpt.isVisible().catch(() => false);301 if (hasOpt) {302 await passphraseOpt.click();303 await popup.waitForTimeout(300);304 }305 const wordCount = popup.locator("input[type='number'], [class*='word']").first();306 const hasWordCount = await wordCount.isVisible().catch(() => false);307 expect(hasWordCount).toBeTruthy();308 await ctx.close();309 });310311 test("PGA03 - exclude ambiguous characters option", async () => {312 const { popup } = await setupVault(ctx);313 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();314 await genBtn.click();315 await popup.waitForTimeout(500);316 const excludeOpt = popup.locator("text=/exclude|ambiguous/i").first();317 const hasExclude = await excludeOpt.isVisible().catch(() => false);318 expect(hasExclude).toBeTruthy();319 await ctx.close();320 });321322 test("PGA04 - history of generated passwords", async () => {323 const { popup } = await setupVault(ctx);324 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();325 await genBtn.click();326 await popup.waitForTimeout(500);327 await popup.locator("text=regenerate, [aria-label*='regenerate' i]").first().click();328 await popup.waitForTimeout(300);329 const history = popup.locator("text=/history|recent/i").first();330 const hasHistory = await history.isVisible().catch(() => false);331 if (hasHistory) expect(true).toBeTruthy();332 else expect(hasHistory).toBeFalsy();333 await ctx.close();334 });335336 test("PGA05 - custom character exclusions", async () => {337 const { popup } = await setupVault(ctx);338 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();339 await genBtn.click();340 await popup.waitForTimeout(500);341 const customExclude = popup.locator("text=/custom|exclude/i").first();342 const hasCustom = await customExclude.isVisible().catch(() => false);343 expect(hasCustom).toBeTruthy();344 await ctx.close();345 });346347 test("PGA06 - pronounceable password option", async () => {348 const { popup } = await setupVault(ctx);349 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();350 await genBtn.click();351 await popup.waitForTimeout(500);352 const pronounceable = popup.locator("text=/pronounceable|readable/i").first();353 const hasPronounceable = await pronounceable.isVisible().catch(() => false);354 expect(hasPronounceable).toBeTruthy();355 await ctx.close();356 });357358 test("PGA07 - pin-only generation option", async () => {359 const { popup } = await setupVault(ctx);360 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();361 await genBtn.click();362 await popup.waitForTimeout(500);363 const pinOpt = popup.locator("text=/pin|digit/i").first();364 const hasPin = await pinOpt.isVisible().catch(() => false);365 expect(hasPin).toBeTruthy();366 await ctx.close();367 });368369 test("PGA08 - generated password entropy is sufficient", async () => {370 const { popup } = await setupVault(ctx);371 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();372 await genBtn.click();373 await popup.waitForTimeout(500);374 const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";375 expect(password.length).toBeGreaterThanOrEqual(12);376 await ctx.close();377 });378});379380test.describe("Password Generator - Entry Integration", () => {381 let ctx: BrowserContext;382 test.afterEach(async () => { await ctx.close(); });383 test.beforeEach(async () => { ctx = await makeContext(); });384385 test("PGI01 - generator works in new entry form", async () => {386 const { popup } = await setupVault(ctx);387 await popup.locator('input[placeholder="search vault..."]').click();388 await popup.getByRole("button", { name: /new entry/i }).click();389 await popup.waitForTimeout(300);390 const genBtn = popup.locator("[aria-label*='generate' i]").first();391 await genBtn.click();392 await popup.waitForTimeout(500);393 const generated = popup.locator("[class*='generated'], [class*='password']").first();394 const hasGen = await generated.isVisible({ timeout: 2000 }).catch(() => false);395 expect(hasGen).toBeTruthy();396 await ctx.close();397 });398399 test("PGI02 - generated password fills entry password field", async () => {400 const { popup } = await setupVault(ctx);401 await popup.locator('input[placeholder="search vault..."]').click();402 await popup.getByRole("button", { name: /new entry/i }).click();403 await popup.waitForTimeout(300);404 const genBtn = popup.locator("[aria-label*='generate' i]").first();405 await genBtn.click();406 await popup.waitForTimeout(500);407 const useBtn = popup.locator("text=/use|apply/i").first();408 await useBtn.click();409 await popup.waitForTimeout(300);410 const passwordField = popup.locator(".peach-new-field-input[placeholder='password']");411 const hasPassword = await passwordField.inputValue().catch(() => "") ?? "";412 expect(hasPassword.length).toBeGreaterThan(0);413 await ctx.close();414 });415416 test("PGI03 - generator accessible from entry detail", async () => {417 const { popup } = await setupVault(ctx);418 await popup.locator('input[placeholder="search vault..."]').click();419 await popup.getByRole("button", { name: /new entry/i }).click();420 await popup.locator(".peach-new-name-input").fill("GenTest");421 await popup.locator('input[placeholder="domain.com"]').fill("https://gentest.com");422 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");423 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");424 await popup.getByRole("button", { name: /create/i }).click();425 await popup.waitForTimeout(1500);426 await popup.locator(".peach-entry-name-button").first().click();427 await popup.waitForTimeout(300);428 const genBtn = popup.locator("[aria-label*='generate' i]").first();429 const hasGen = await genBtn.isVisible().catch(() => false);430 expect(hasGen).toBeTruthy();431 await ctx.close();432 });433434 test("PGI04 - regenerate updates entry password", async () => {435 const { popup } = await setupVault(ctx);436 await popup.locator('input[placeholder="search vault..."]').click();437 await popup.getByRole("button", { name: /new entry/i }).click();438 await popup.waitForTimeout(300);439 const genBtn = popup.locator("[aria-label*='generate' i]").first();440 await genBtn.click();441 await popup.waitForTimeout(500);442 const pass1 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";443 const regenBtn = popup.locator("text=regenerate").first();444 await regenBtn.click();445 await popup.waitForTimeout(300);446 const pass2 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";447 expect(pass1).not.toEqual(pass2);448 await ctx.close();449 });450451 test("PGI05 - password copied notification", async () => {452 const { popup } = await setupVault(ctx);453 const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();454 await genBtn.click();455 await popup.waitForTimeout(500);456 const copyBtn = popup.locator("[aria-label*='copy' i]").first();457 await copyBtn.click();458 await popup.waitForTimeout(300);459 const notification = popup.locator("text=/copied|\\u2713|copied to clipboard/i");460 const hasNotification = await notification.isVisible({ timeout: 2000 }).catch(() => false);461 expect(hasNotification).toBeTruthy();462 await ctx.close();463 });464});