1import { test, expect, 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 makeContext() {12 const { chromium } = await import("playwright");13 const userDataDir = join(os.tmpdir(), `peach-kbd-${Date.now()}`);14 await fs.mkdir(userDataDir, { recursive: true });15 return chromium.launchPersistentContext(userDataDir, {16 headless: false,17 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],18 });19}2021async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {22 const start = Date.now();23 let sw = ctx.serviceWorkers()[0];24 while (!sw && Date.now() - start < maxWait) {25 await new Promise(r => setTimeout(r, 500));26 sw = ctx.serviceWorkers()[0];27 }28 if (!sw) throw new Error(`No SW after ${maxWait}ms`);29 return new URL(sw.url()).host;30}3132async function setupVault(ctx: BrowserContext): Promise<{ popup: any, extId: string }> {33 const extId = await waitForSW(ctx);34 const popup = await ctx.newPage();35 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);36 await popup.waitForTimeout(3000);37 const pw = popup.locator("#master-password");38 if (await pw.isVisible()) {39 await pw.fill("password123");40 await popup.locator("#confirm-password").fill("password123");41 await popup.getByRole("button", { name: /create vault/i }).click();42 await popup.waitForTimeout(2000);43 }44 return { popup, extId };45}4647test.describe("Keyboard Navigation", () => {4849 test("KB01 - enter submits password on lock screen", async () => {50 const ctx = await makeContext();51 await setupVault(ctx);52 const newPopup = await ctx.newPage();53 const extId = await waitForSW(ctx);54 await newPopup.goto(`chrome-extension://${extId}/src/popup/index.html`);55 await newPopup.waitForTimeout(2000);56 await newPopup.locator("#master-password").fill("password123");57 await newPopup.keyboard.press("Enter");58 await newPopup.waitForTimeout(2000);59 const entries = newPopup.locator("[class*=entry]");60 await expect(entries.first()).toBeVisible({ timeout: 5000 });61 await ctx.close();62 });6364 test("KB02 - tab navigates through fields", async () => {65 const ctx = await makeContext();66 const { popup } = await setupVault(ctx);67 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();68 await popup.waitForTimeout(500);69 await popup.keyboard.press("Tab");70 await popup.keyboard.press("Tab");71 const focused = await popup.evaluate(() => document.activeElement?.tagName);72 expect(focused).toBeTruthy();73 await ctx.close();74 });7576 test("KB03 - escape closes settings panel", async () => {77 const ctx = await makeContext();78 const { popup } = await setupVault(ctx);79 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();80 await popup.waitForTimeout(500);81 await popup.keyboard.press("Escape");82 await popup.waitForTimeout(300);83 const overlay = popup.locator(".peach-overlay-view");84 const isHidden = await overlay.isHidden().catch(() => true);85 expect(isHidden).toBeTruthy();86 await ctx.close();87 });8889 test("KB04 - arrow keys navigate entry list", async () => {90 const ctx = await makeContext();91 const { popup } = await setupVault(ctx);92 for (const name of ["Alpha", "Beta", "Gamma"]) {93 await popup.locator('input[placeholder="search vault..."]').click();94 await popup.getByRole("button", { name: /new entry/i }).click();95 await popup.locator(".peach-new-name-input").fill(name);96 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);97 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);98 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");99 await popup.getByRole("button", { name: /create/i }).click();100 await popup.waitForTimeout(500);101 }102 await popup.locator('input[placeholder="search vault..."]').click();103 await popup.keyboard.press("ArrowDown");104 await popup.waitForTimeout(200);105 await ctx.close();106 });107108 test("KB05 - enter opens entry from list", async () => {109 const ctx = await makeContext();110 const { popup } = await setupVault(ctx);111 await popup.locator('input[placeholder="search vault..."]').click();112 await popup.getByRole("button", { name: /new entry/i }).click();113 await popup.locator(".peach-new-name-input").fill("EnterTest");114 await popup.locator('input[placeholder="domain.com"]').fill("https://entertest.com");115 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");116 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");117 await popup.getByRole("button", { name: /create/i }).click();118 await popup.waitForTimeout(1000);119 await popup.locator('input[placeholder="search vault..."]').click();120 await popup.waitForTimeout(300);121 await popup.keyboard.press("Enter");122 await popup.waitForTimeout(500);123 await ctx.close();124 });125126 test("KB06 - ctrl+k focuses search", async () => {127 const ctx = await makeContext();128 const { popup } = await setupVault(ctx);129 await popup.keyboard.press("Control+k");130 await popup.waitForTimeout(200);131 const searchFocused = await popup.evaluate(() => {132 const el = document.activeElement;133 return el?.classList.contains("peach-search-input") || el?.closest(".peach-search-input");134 });135 expect(searchFocused).toBeTruthy();136 await ctx.close();137 });138139 test("KB07 - backspace clears search", async () => {140 const ctx = await makeContext();141 const { popup } = await setupVault(ctx);142 await popup.locator('input[placeholder="search vault..."]').click();143 await popup.locator('input[placeholder="search vault..."]').fill("test");144 await popup.keyboard.press("Backspace");145 await popup.keyboard.press("Backspace");146 await popup.keyboard.press("Backspace");147 await popup.keyboard.press("Backspace");148 await ctx.close();149 });150});151152test.describe("Edge Cases & Error Handling", () => {153154 test("EDGE01 - entry with very long title", async () => {155 const ctx = await makeContext();156 const { popup } = await setupVault(ctx);157 await popup.locator('input[placeholder="search vault..."]').click();158 await popup.getByRole("button", { name: /new entry/i }).click();159 const longTitle = "A".repeat(200);160 await popup.locator(".peach-new-name-input").fill(longTitle);161 await popup.locator('input[placeholder="domain.com"]').fill("https://longtitle.com");162 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");163 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");164 await popup.getByRole("button", { name: /create/i }).click();165 await popup.waitForTimeout(1500);166 await ctx.close();167 });168169 test("EDGE02 - entry with unicode in all fields", async () => {170 const ctx = await makeContext();171 const { popup } = await setupVault(ctx);172 await popup.locator('input[placeholder="search vault..."]').click();173 await popup.getByRole("button", { name: /new entry/i }).click();174 await popup.locator(".peach-new-name-input").fill("日本語テスト");175 await popup.locator('input[placeholder="domain.com"]').fill("https://日本語.com");176 await popup.locator('input[placeholder="username or email"]').fill("用户@例子.测试");177 await popup.locator(".peach-new-field-input[placeholder='password']").fill("密码🔐");178 await popup.getByRole("button", { name: /create/i }).click();179 await popup.waitForTimeout(1500);180 await expect(popup.locator("text=日本語テスト")).toBeVisible({ timeout: 3000 });181 await ctx.close();182 });183184 test("EDGE03 - very short password accepted", async () => {185 const ctx = await makeContext();186 const { popup } = await setupVault(ctx);187 await popup.locator('input[placeholder="search vault..."]').click();188 await popup.getByRole("button", { name: /new entry/i }).click();189 await popup.locator(".peach-new-name-input").fill("ShortPass");190 await popup.locator('input[placeholder="domain.com"]').fill("https://short.com");191 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");192 await popup.locator(".peach-new-field-input[placeholder='password']").fill("a");193 await popup.getByRole("button", { name: /create/i }).click();194 await popup.waitForTimeout(1500);195 await ctx.close();196 });197198 test("EDGE04 - entry with special chars in domain", async () => {199 const ctx = await makeContext();200 const { popup } = await setupVault(ctx);201 await popup.locator('input[placeholder="search vault..."]').click();202 await popup.getByRole("button", { name: /new entry/i }).click();203 await popup.locator(".peach-new-name-input").fill("SpecialDomain");204 await popup.locator('input[placeholder="domain.com"]').fill("https://sub.domain.with-dashes.and_underscores.example.com/path?query=value");205 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");206 await popup.locator(".peach-new-field-input[placeholder='password']").fill("specialpass");207 await popup.getByRole("button", { name: /create/i }).click();208 await popup.waitForTimeout(1500);209 await ctx.close();210 });211212 test("EDGE05 - entry with no username", async () => {213 const ctx = await makeContext();214 const { popup } = await setupVault(ctx);215 await popup.locator('input[placeholder="search vault..."]').click();216 await popup.getByRole("button", { name: /new entry/i }).click();217 await popup.locator(".peach-new-name-input").fill("NoUsername");218 await popup.locator('input[placeholder="domain.com"]').fill("https://nouser.com");219 await popup.locator('input[placeholder="username or email"]').fill("");220 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");221 await popup.getByRole("button", { name: /create/i }).click();222 await popup.waitForTimeout(1500);223 await ctx.close();224 });225226 test("EDGE06 - entry with no domain", async () => {227 const ctx = await makeContext();228 const { popup } = await setupVault(ctx);229 await popup.locator('input[placeholder="search vault..."]').click();230 await popup.getByRole("button", { name: /new entry/i }).click();231 await popup.locator(".peach-new-name-input").fill("NoDomain");232 await popup.locator('input[placeholder="domain.com"]').fill("");233 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");234 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");235 await popup.getByRole("button", { name: /create/i }).click();236 await popup.waitForTimeout(1500);237 await ctx.close();238 });239240 test("EDGE07 - rapid add entry button clicks", async () => {241 const ctx = await makeContext();242 const { popup } = await setupVault(ctx);243 const addBtn = popup.getByRole("button", { name: /new entry/i });244 await addBtn.click();245 await addBtn.click();246 await addBtn.click();247 await popup.waitForTimeout(500);248 const modal = popup.locator(".peach-new-body");249 await expect(modal).toBeVisible({ timeout: 3000 });250 await ctx.close();251 });252253 test("EDGE08 - search with special regex chars", async () => {254 const ctx = await makeContext();255 const { popup } = await setupVault(ctx);256 for (const name of ["Test[0]", "Test(1)", "Test{2}"]) {257 await popup.locator('input[placeholder="search vault..."]').click();258 await popup.getByRole("button", { name: /new entry/i }).click();259 await popup.locator(".peach-new-name-input").fill(name);260 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase().replace(/[^a-z]/g, "")}.com`);261 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);262 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");263 await popup.getByRole("button", { name: /create/i }).click();264 await popup.waitForTimeout(500);265 }266 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();267 await searchInput.click();268 await searchInput.fill("[0]");269 await popup.waitForTimeout(300);270 await ctx.close();271 });272273 test("EDGE09 - copy button during loading state", async () => {274 const ctx = await makeContext();275 const { popup } = await setupVault(ctx);276 await popup.locator('input[placeholder="search vault..."]').click();277 await popup.getByRole("button", { name: /new entry/i }).click();278 await popup.locator(".peach-new-name-input").fill("CopyLoading");279 await popup.locator('input[placeholder="domain.com"]').fill("https://copyloading.com");280 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");281 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");282 await popup.getByRole("button", { name: /create/i }).click();283 await popup.waitForTimeout(1500);284 const copyBtn = popup.locator(".peach-copy-btn").first();285 if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {286 await copyBtn.click();287 await popup.waitForTimeout(500);288 }289 await ctx.close();290 });291292 test("EDGE10 - unlock with whitespace password", async () => {293 const ctx = await makeContext();294 const extId = await waitForSW(ctx);295 const popup = await ctx.newPage();296 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);297 await popup.waitForTimeout(2000);298 await popup.locator("#master-password").fill(" ");299 await popup.getByRole("button", { name: /unlock/i }).click();300 await popup.waitForTimeout(500);301 const errorMsg = popup.locator(".peach-hint-error, .peach-hint");302 await errorMsg.first().isVisible({ timeout: 2000 }).catch(() => false);303 await ctx.close();304 });305});306307test.describe("Import Export Round-trip", () => {308309 test("IMP01 - import CSV and verify entries", async () => {310 const ctx = await makeContext();311 const { popup } = await setupVault(ctx);312 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();313 await popup.waitForTimeout(300);314 await popup.getByRole("button", { name: /import/i }).click();315 await popup.waitForTimeout(500);316 const fileInput = popup.locator('input[type="file"]');317 const csvContent = "title,username,password,url\nTestImport,importuser,importpass,https://import.com";318 await fileInput.setInputFiles({319 name: "test.csv",320 mimeType: "text/csv",321 buffer: Buffer.from(csvContent)322 });323 await popup.waitForTimeout(1000);324 await ctx.close();325 });326327 test("IMP02 - export creates valid JSON file", async () => {328 const ctx = await makeContext();329 const { popup } = await setupVault(ctx);330 await popup.locator('input[placeholder="search vault..."]').click();331 await popup.getByRole("button", { name: /new entry/i }).click();332 await popup.locator(".peach-new-name-input").fill("ExportTest");333 await popup.locator('input[placeholder="domain.com"]').fill("https://exporttest.com");334 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");335 await popup.locator(".peach-new-field-input[placeholder='password']").fill("exportpass");336 await popup.getByRole("button", { name: /create/i }).click();337 await popup.waitForTimeout(1500);338 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();339 await popup.waitForTimeout(300);340 await popup.getByRole("button", { name: /export/i }).click();341 await popup.waitForTimeout(500);342 await ctx.close();343 });344345 test("IMP03 - empty vault export works", async () => {346 const ctx = await makeContext();347 const { popup } = await setupVault(ctx);348 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();349 await popup.waitForTimeout(300);350 await popup.getByRole("button", { name: /export/i }).click();351 await popup.waitForTimeout(500);352 await ctx.close();353 });354355 test("IMP04 - import 1Password JSON format", async () => {356 const ctx = await makeContext();357 const { popup } = await setupVault(ctx);358 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();359 await popup.waitForTimeout(300);360 await popup.getByRole("button", { name: /import/i }).click();361 await popup.waitForTimeout(500);362 const fileInput = popup.locator('input[type="file"]');363 const json1pContent = JSON.stringify({364 accounts: [{365 vaults: [{366 items: [{367 overview: { title: "1P Test", url: "https://1p.com" },368 details: {369 loginFields: [370 { designation: "username", value: "testuser" },371 { designation: "password", value: "testpass" }372 ]373 }374 }]375 }]376 }]377 });378 await fileInput.setInputFiles({379 name: "1p-export.json",380 mimeType: "application/json",381 buffer: Buffer.from(json1pContent)382 });383 await popup.waitForTimeout(1000);384 await ctx.close();385 });386});387388test.describe("Password Generator", () => {389390 test("GEN01 - generates passwords of exact length", async () => {391 const ctx = await makeContext();392 const { popup } = await setupVault(ctx);393 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();394 await popup.waitForTimeout(300);395 await popup.getByRole("button", { name: /password generator/i }).click();396 await popup.waitForTimeout(500);397 const slider = popup.locator(".peach-pwgen-slider");398 await slider.fill("32");399 await popup.waitForTimeout(300);400 const output = await popup.locator(".peach-pwgen-output").textContent();401 expect(output?.length).toBe(32);402 await ctx.close();403 });404405 test("GEN02 - regenerate produces new password", async () => {406 const ctx = await makeContext();407 const { popup } = await setupVault(ctx);408 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();409 await popup.waitForTimeout(300);410 await popup.getByRole("button", { name: /password generator/i }).click();411 await popup.waitForTimeout(500);412 const pwd1 = await popup.locator(".peach-pwgen-output").textContent();413 await popup.locator("button[aria-label='Regenerate']").click();414 await popup.waitForTimeout(300);415 const pwd2 = await popup.locator(".peach-pwgen-output").textContent();416 expect(pwd1).not.toEqual(pwd2);417 await ctx.close();418 });419420 test("GEN03 - toggle uppercase on/off", async () => {421 const ctx = await makeContext();422 const { popup } = await setupVault(ctx);423 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();424 await popup.waitForTimeout(300);425 await popup.getByRole("button", { name: /password generator/i }).click();426 await popup.waitForTimeout(500);427 const toggles = popup.locator(".peach-pwgen-toggle-row");428 const count = await toggles.count();429 if (count >= 2) {430 await toggles.nth(0).click();431 await popup.waitForTimeout(200);432 }433 await ctx.close();434 });435436 test("GEN04 - toggle numbers on/off", async () => {437 const ctx = await makeContext();438 const { popup } = await setupVault(ctx);439 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();440 await popup.waitForTimeout(300);441 await popup.getByRole("button", { name: /password generator/i }).click();442 await popup.waitForTimeout(500);443 const toggles = popup.locator(".peach-pwgen-toggle-row");444 const count = await toggles.count();445 if (count >= 3) {446 await toggles.nth(2).click();447 await popup.waitForTimeout(200);448 }449 await ctx.close();450 });451452 test("GEN05 - toggle symbols on/off", async () => {453 const ctx = await makeContext();454 const { popup } = await setupVault(ctx);455 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();456 await popup.waitForTimeout(300);457 await popup.getByRole("button", { name: /password generator/i }).click();458 await popup.waitForTimeout(500);459 const toggles = popup.locator(".peach-pwgen-toggle-row");460 const count = await toggles.count();461 if (count >= 4) {462 await toggles.nth(3).click();463 await popup.waitForTimeout(200);464 }465 await ctx.close();466 });467468 test("GEN06 - min length 8 enforced", async () => {469 const ctx = await makeContext();470 const { popup } = await setupVault(ctx);471 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();472 await popup.waitForTimeout(300);473 await popup.getByRole("button", { name: /password generator/i }).click();474 await popup.waitForTimeout(500);475 const slider = popup.locator(".peach-pwgen-slider");476 await slider.fill("4");477 await popup.waitForTimeout(300);478 const output = await popup.locator(".peach-pwgen-output").textContent();479 expect(output?.length).toBeGreaterThanOrEqual(8);480 await ctx.close();481 });482483 test("GEN07 - max length 64 enforced", async () => {484 const ctx = await makeContext();485 const { popup } = await setupVault(ctx);486 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();487 await popup.waitForTimeout(300);488 await popup.getByRole("button", { name: /password generator/i }).click();489 await popup.waitForTimeout(500);490 const slider = popup.locator(".peach-pwgen-slider");491 await slider.fill("128");492 await popup.waitForTimeout(300);493 const output = await popup.locator(".peach-pwgen-output").textContent();494 expect(output?.length).toBeLessThanOrEqual(64);495 await ctx.close();496 });497498 test("GEN08 - use password fills into entry", async () => {499 const ctx = await makeContext();500 const { popup } = await setupVault(ctx);501 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();502 await popup.waitForTimeout(300);503 await popup.getByRole("button", { name: /password generator/i }).click();504 await popup.waitForTimeout(500);505 const useBtn = popup.getByRole("button", { name: /use/i });506 if (await useBtn.isVisible({ timeout: 2000 }).catch(() => false)) {507 await useBtn.click();508 await popup.waitForTimeout(500);509 }510 await ctx.close();511 });512});