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-kb-${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("Global Keyboard Shortcuts", () => {48 let ctx: BrowserContext;49 test.afterEach(async () => { await ctx.close(); });50 test.beforeEach(async () => { ctx = await makeContext(); });5152 test("KBG01 - Enter submits master password", async () => {53 const { popup: _popup } = await setupVault(ctx);54 const newPage = await ctx.newPage();55 const extId = await waitForSW(ctx);56 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);57 await newPage.waitForTimeout(2000);58 await newPage.locator("#master-password").fill("password123");59 await newPage.keyboard.press("Enter");60 await newPage.waitForTimeout(2000);61 const entries = newPage.locator("[class*=entry]");62 await expect(entries.first()).toBeVisible({ timeout: 5000 });63 await ctx.close();64 });6566 test("KBG02 - Ctrl+Shift+P opens extension popup", async () => {67 const { popup } = await setupVault(ctx);68 const isVisible = await popup.isVisible();69 expect(isVisible).toBe(true);70 await ctx.close();71 });7273 test("KBG03 - Escape from lock screen does nothing", async () => {74 const { popup: _popup } = await setupVault(ctx);75 const newPage = await ctx.newPage();76 const extId = await waitForSW(ctx);77 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);78 await newPage.waitForTimeout(2000);79 await newPage.locator("#master-password").fill("password123");80 await newPage.keyboard.press("Escape");81 await newPage.waitForTimeout(500);82 const pw = newPage.locator("#master-password");83 const isVisible = await pw.isVisible({ timeout: 2000 }).catch(() => false);84 expect(isVisible).toBe(true);85 await ctx.close();86 });8788 test("KBG04 - Ctrl+N opens new entry dialog", async () => {89 const { popup } = await setupVault(ctx);90 await popup.keyboard.press("Control+n");91 await popup.waitForTimeout(500);92 const newBody = popup.locator(".peach-new-body");93 const isVisible = await newBody.isVisible({ timeout: 3000 }).catch(() => false);94 expect(isVisible).toBeTruthy();95 await ctx.close();96 });9798 test("KBG05 - Ctrl+F focuses search", async () => {99 const { popup } = await setupVault(ctx);100 await popup.keyboard.press("Control+f");101 await popup.waitForTimeout(200);102 const focused = await popup.evaluate(() => document.activeElement?.className);103 expect(focused).toMatch(/search/i);104 await ctx.close();105 });106107 test("KBG06 - Ctrl+, opens settings", async () => {108 const { popup } = await setupVault(ctx);109 await popup.keyboard.press("Control+,");110 await popup.waitForTimeout(500);111 const settingsTitle = popup.locator(".peach-overlay-title, text=settings").first();112 const isVisible = await settingsTitle.isVisible({ timeout: 3000 }).catch(() => false);113 expect(isVisible).toBeTruthy();114 await ctx.close();115 });116117 test("KBG07 - Ctrl+L locks vault", async () => {118 const { popup } = await setupVault(ctx);119 await popup.keyboard.press("Control+l");120 await popup.waitForTimeout(1500);121 const pw = popup.locator("#master-password");122 const isVisible = await pw.isVisible({ timeout: 3000 }).catch(() => false);123 expect(isVisible).toBeTruthy();124 await ctx.close();125 });126});127128test.describe("Entry Form Keyboard Navigation", () => {129 let ctx: BrowserContext;130 test.afterEach(async () => { await ctx.close(); });131 test.beforeEach(async () => { ctx = await makeContext(); });132133 test("KBE01 - Tab moves through add entry form", async () => {134 const { popup } = await setupVault(ctx);135 await popup.locator('input[placeholder="search vault..."]').click();136 await popup.getByRole("button", { name: /new entry/i }).click();137 await popup.waitForTimeout(300);138 const nameInput = popup.locator(".peach-new-name-input");139 await nameInput.focus();140 await popup.keyboard.press("Tab");141 await popup.waitForTimeout(100);142 const focused = await popup.evaluate(() => document.activeElement?.tagName);143 expect(focused).toBe("INPUT");144 await ctx.close();145 });146147 test("KBE02 - Tab from name goes to domain", async () => {148 const { popup } = await setupVault(ctx);149 await popup.locator('input[placeholder="search vault..."]').click();150 await popup.getByRole("button", { name: /new entry/i }).click();151 await popup.waitForTimeout(300);152 await popup.locator(".peach-new-name-input").focus();153 await popup.keyboard.press("Tab");154 await popup.waitForTimeout(100);155 const focusedPlaceholder = await popup.evaluate(() => (document.activeElement as HTMLInputElement)?.placeholder);156 expect(focusedPlaceholder).toContain("domain");157 await ctx.close();158 });159160 test("KBE03 - Tab from domain goes to username", async () => {161 const { popup } = await setupVault(ctx);162 await popup.locator('input[placeholder="search vault..."]').click();163 await popup.getByRole("button", { name: /new entry/i }).click();164 await popup.waitForTimeout(300);165 await popup.locator(".peach-new-name-input").focus();166 await popup.keyboard.press("Tab");167 await popup.keyboard.press("Tab");168 await popup.waitForTimeout(100);169 const focusedPlaceholder = await popup.evaluate(() => (document.activeElement as HTMLInputElement)?.placeholder);170 expect(focusedPlaceholder).toContain("username");171 await ctx.close();172 });173174 test("KBE04 - Tab from username goes to password", async () => {175 const { popup } = await setupVault(ctx);176 await popup.locator('input[placeholder="search vault..."]').click();177 await popup.getByRole("button", { name: /new entry/i }).click();178 await popup.waitForTimeout(300);179 await popup.locator(".peach-new-name-input").focus();180 await popup.keyboard.press("Tab");181 await popup.keyboard.press("Tab");182 await popup.keyboard.press("Tab");183 await popup.waitForTimeout(100);184 const focusedClass = await popup.evaluate(() => document.activeElement?.className);185 expect(focusedClass).toContain("peach-new-field-input");186 await ctx.close();187 });188189 test("KBE05 - Shift+Tab goes backwards", async () => {190 const { popup } = await setupVault(ctx);191 await popup.locator('input[placeholder="search vault..."]').click();192 await popup.getByRole("button", { name: /new entry/i }).click();193 await popup.waitForTimeout(300);194 const pwField = popup.locator(".peach-new-field-input[placeholder='password']");195 await pwField.focus();196 await popup.keyboard.press("Shift+Tab");197 await popup.waitForTimeout(100);198 const focused = await popup.evaluate(() => document.activeElement?.tagName);199 expect(focused).toBe("INPUT");200 await ctx.close();201 });202203 test("KBE06 - Enter submits add entry form", async () => {204 const { popup } = await setupVault(ctx);205 await popup.locator('input[placeholder="search vault..."]').click();206 await popup.getByRole("button", { name: /new entry/i }).click();207 await popup.waitForTimeout(300);208 await popup.locator(".peach-new-name-input").fill("EnterSubmit");209 await popup.locator('input[placeholder="domain.com"]').fill("https://enter.com");210 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");211 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");212 await popup.keyboard.press("Enter");213 await popup.waitForTimeout(1500);214 await expect(popup.locator("text=EnterSubmit")).toBeVisible({ timeout: 3000 });215 await ctx.close();216 });217218 test("KBE07 - Escape closes add entry dialog", async () => {219 const { popup } = await setupVault(ctx);220 await popup.locator('input[placeholder="search vault..."]').click();221 await popup.getByRole("button", { name: /new entry/i }).click();222 await popup.waitForTimeout(300);223 await popup.keyboard.press("Escape");224 await popup.waitForTimeout(300);225 const newBody = popup.locator(".peach-new-body");226 const isHidden = await newBody.isHidden().catch(() => false);227 expect(isHidden).toBeTruthy();228 await ctx.close();229 });230});231232test.describe("Vault List Keyboard Navigation", () => {233 let ctx: BrowserContext;234 test.afterEach(async () => { await ctx.close(); });235 test.beforeEach(async () => { ctx = await makeContext(); });236237 test("KBV01 - ArrowDown moves selection down", async () => {238 const { popup } = await setupVault(ctx);239 for (const name of ["First", "Second", "Third"]) {240 await popup.locator('input[placeholder="search vault..."]').click();241 await popup.getByRole("button", { name: /new entry/i }).click();242 await popup.locator(".peach-new-name-input").fill(name);243 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);244 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);245 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");246 await popup.getByRole("button", { name: /create/i }).click();247 await popup.waitForTimeout(800);248 }249 await popup.locator('input[placeholder="search vault..."]').click();250 await popup.waitForTimeout(200);251 await popup.keyboard.press("ArrowDown");252 await popup.waitForTimeout(200);253 const activeRow = popup.locator(".peach-entry-row.is-active");254 const hasActive = await activeRow.isVisible({ timeout: 2000 }).catch(() => false);255 expect(hasActive).toBeTruthy();256 await ctx.close();257 });258259 test("KBV02 - ArrowUp moves selection up", async () => {260 const { popup } = await setupVault(ctx);261 for (const name of ["RowUp1", "RowUp2"]) {262 await popup.locator('input[placeholder="search vault..."]').click();263 await popup.getByRole("button", { name: /new entry/i }).click();264 await popup.locator(".peach-new-name-input").fill(name);265 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);266 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);267 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");268 await popup.getByRole("button", { name: /create/i }).click();269 await popup.waitForTimeout(800);270 }271 await popup.locator('input[placeholder="search vault..."]').click();272 await popup.waitForTimeout(200);273 await popup.keyboard.press("ArrowDown");274 await popup.keyboard.press("ArrowDown");275 await popup.waitForTimeout(200);276 await popup.keyboard.press("ArrowUp");277 await popup.waitForTimeout(200);278 const activeRow = popup.locator(".peach-entry-row.is-active");279 const hasActive = await activeRow.isVisible({ timeout: 2000 }).catch(() => false);280 expect(hasActive).toBeTruthy();281 await ctx.close();282 });283284 test("KBV03 - Enter opens selected entry", async () => {285 const { popup } = await setupVault(ctx);286 await popup.locator('input[placeholder="search vault..."]').click();287 await popup.getByRole("button", { name: /new entry/i }).click();288 await popup.locator(".peach-new-name-input").fill("OpenMe");289 await popup.locator('input[placeholder="domain.com"]').fill("https://open.com");290 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");291 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");292 await popup.getByRole("button", { name: /create/i }).click();293 await popup.waitForTimeout(1500);294 await popup.locator('input[placeholder="search vault..."]').click();295 await popup.waitForTimeout(200);296 await popup.keyboard.press("Enter");297 await popup.waitForTimeout(500);298 const usernameField = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();299 await expect(usernameField).toBeVisible({ timeout: 3000 });300 await ctx.close();301 });302303 test("KBV04 - Home key goes to first entry", async () => {304 const { popup } = await setupVault(ctx);305 for (const name of ["AlphaHome", "BetaHome"]) {306 await popup.locator('input[placeholder="search vault..."]').click();307 await popup.getByRole("button", { name: /new entry/i }).click();308 await popup.locator(".peach-new-name-input").fill(name);309 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);310 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);311 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");312 await popup.getByRole("button", { name: /create/i }).click();313 await popup.waitForTimeout(800);314 }315 await popup.locator('input[placeholder="search vault..."]').click();316 await popup.keyboard.press("End");317 await popup.waitForTimeout(200);318 await popup.keyboard.press("Home");319 await popup.waitForTimeout(200);320 await ctx.close();321 });322323 test("KBV05 - End key goes to last entry", async () => {324 const { popup } = await setupVault(ctx);325 for (const name of ["FirstEnd", "SecondEnd", "ThirdEnd"]) {326 await popup.locator('input[placeholder="search vault..."]').click();327 await popup.getByRole("button", { name: /new entry/i }).click();328 await popup.locator(".peach-new-name-input").fill(name);329 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);330 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);331 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");332 await popup.getByRole("button", { name: /create/i }).click();333 await popup.waitForTimeout(800);334 }335 await popup.locator('input[placeholder="search vault..."]').click();336 await popup.keyboard.press("End");337 await popup.waitForTimeout(200);338 await ctx.close();339 });340341 test("KBV06 - Delete key triggers delete confirmation", async () => {342 const { popup } = await setupVault(ctx);343 await popup.locator('input[placeholder="search vault..."]').click();344 await popup.getByRole("button", { name: /new entry/i }).click();345 await popup.locator(".peach-new-name-input").fill("DeleteKey");346 await popup.locator('input[placeholder="domain.com"]').fill("https://delkey.com");347 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");348 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");349 await popup.getByRole("button", { name: /create/i }).click();350 await popup.waitForTimeout(1500);351 await ctx.close();352 });353354 test("KBV07 - Ctrl+A selects all in search", async () => {355 const { popup } = await setupVault(ctx);356 await popup.locator('input[placeholder="search vault..."]').click();357 await popup.locator('input[placeholder="search vault..."]').fill("test");358 await popup.waitForTimeout(100);359 await popup.keyboard.press("Control+a");360 await popup.waitForTimeout(100);361 const selected = await popup.evaluate(() => {362 const input = document.activeElement as HTMLInputElement;363 return input?.selectionStart === 0 && input?.selectionEnd === input?.value?.length;364 });365 expect(selected).toBeTruthy();366 await ctx.close();367 });368});369370test.describe("Settings Panel Keyboard", () => {371 let ctx: BrowserContext;372 test.afterEach(async () => { await ctx.close(); });373 test.beforeEach(async () => { ctx = await makeContext(); });374375 test("KBS01 - Tab navigates settings options", async () => {376 const { popup } = await setupVault(ctx);377 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();378 await popup.waitForTimeout(500);379 await popup.keyboard.press("Tab");380 await popup.waitForTimeout(100);381 const focused = await popup.evaluate(() => document.activeElement?.tagName);382 expect(focused).toBeTruthy();383 await ctx.close();384 });385386 test("KBS02 - Arrow keys navigate setting options", async () => {387 const { popup } = await setupVault(ctx);388 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();389 await popup.waitForTimeout(500);390 await popup.locator("text=auto-lock").click();391 await popup.waitForTimeout(300);392 await popup.keyboard.press("ArrowDown");393 await popup.waitForTimeout(200);394 await popup.keyboard.press("Escape");395 await ctx.close();396 });397398 test("KBS03 - Escape closes settings panel", async () => {399 const { popup } = await setupVault(ctx);400 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();401 await popup.waitForTimeout(500);402 await popup.keyboard.press("Escape");403 await popup.waitForTimeout(300);404 const overlay = popup.locator(".peach-overlay-view");405 const isHidden = await overlay.isHidden().catch(() => true);406 expect(isHidden).toBeTruthy();407 await ctx.close();408 });409410 test("KBS04 - Enter activates focused option", async () => {411 const { popup } = await setupVault(ctx);412 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();413 await popup.waitForTimeout(500);414 const lockVault = popup.locator("text=lock vault").first();415 if (await lockVault.isVisible({ timeout: 2000 }).catch(() => false)) {416 await lockVault.focus();417 await popup.keyboard.press("Enter");418 await popup.waitForTimeout(1000);419 const pwField = popup.locator("#master-password");420 const isVisible = await pwField.isVisible({ timeout: 3000 }).catch(() => false);421 expect(isVisible).toBeTruthy();422 }423 await ctx.close();424 });425426 test("KBS05 - Space activates toggle switches", async () => {427 const { popup } = await setupVault(ctx);428 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();429 await popup.waitForTimeout(500);430 await popup.locator("text=lock on close").click();431 await popup.waitForTimeout(300);432 const toggle = popup.locator("[role='switch' i]").first();433 if (await toggle.isVisible({ timeout: 2000 }).catch(() => false)) {434 await toggle.focus();435 await popup.keyboard.press(" ");436 await popup.waitForTimeout(200);437 }438 await ctx.close();439 });440});441442test.describe("Password Generator Keyboard", () => {443 let ctx: BrowserContext;444 test.afterEach(async () => { await ctx.close(); });445 test.beforeEach(async () => { ctx = await makeContext(); });446447 test("KBG01 - Tab navigates generator options", async () => {448 const { popup } = await setupVault(ctx);449 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();450 await popup.waitForTimeout(300);451 await popup.getByRole("button", { name: /password generator/i }).click();452 await popup.waitForTimeout(500);453 await popup.keyboard.press("Tab");454 await popup.waitForTimeout(100);455 const focused = await popup.evaluate(() => document.activeElement?.tagName);456 expect(focused).toBeTruthy();457 await ctx.close();458 });459460 test("KBG02 - Arrow keys adjust length slider", async () => {461 const { popup } = await setupVault(ctx);462 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();463 await popup.waitForTimeout(300);464 await popup.getByRole("button", { name: /password generator/i }).click();465 await popup.waitForTimeout(500);466 const slider = popup.locator(".peach-pwgen-slider");467 await slider.focus();468 await popup.keyboard.press("ArrowRight");469 await popup.waitForTimeout(100);470 await popup.keyboard.press("ArrowLeft");471 await popup.waitForTimeout(100);472 await ctx.close();473 });474475 test("KBG03 - Space toggles option switches", async () => {476 const { popup } = await setupVault(ctx);477 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();478 await popup.waitForTimeout(300);479 await popup.getByRole("button", { name: /password generator/i }).click();480 await popup.waitForTimeout(500);481 const toggleRow = popup.locator(".peach-pwgen-toggle-row").first();482 if (await toggleRow.isVisible({ timeout: 2000 }).catch(() => false)) {483 await toggleRow.focus();484 await popup.keyboard.press(" ");485 await popup.waitForTimeout(200);486 }487 await ctx.close();488 });489490 test("KBG04 - Escape closes generator", async () => {491 const { popup } = await setupVault(ctx);492 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();493 await popup.waitForTimeout(300);494 await popup.getByRole("button", { name: /password generator/i }).click();495 await popup.waitForTimeout(500);496 await popup.keyboard.press("Escape");497 await popup.waitForTimeout(300);498 const output = popup.locator(".peach-pwgen-output");499 await output.isHidden().catch(() => true);500 await ctx.close();501 });502503 test("KBG05 - Enter copies generated password", async () => {504 const { popup } = await setupVault(ctx);505 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();506 await popup.waitForTimeout(300);507 await popup.getByRole("button", { name: /password generator/i }).click();508 await popup.waitForTimeout(500);509 const copyBtn = popup.getByRole("button", { name: /copy/i }).first();510 if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {511 await copyBtn.focus();512 await popup.keyboard.press("Enter");513 await popup.waitForTimeout(300);514 }515 await ctx.close();516 });517});518519test.describe("Lock Screen Keyboard", () => {520 let ctx: BrowserContext;521 test.afterEach(async () => { await ctx.close(); });522 test.beforeEach(async () => { ctx = await makeContext(); });523524 test("KBL01 - Tab navigates lock screen", async () => {525 const { popup: _popup } = await setupVault(ctx);526 const newPage = await ctx.newPage();527 const extId = await waitForSW(ctx);528 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);529 await newPage.waitForTimeout(2000);530 await newPage.keyboard.press("Tab");531 await newPage.waitForTimeout(100);532 const focused = await newPage.evaluate(() => document.activeElement?.tagName);533 expect(focused).toBeTruthy();534 await ctx.close();535 });536537 test("KBL02 - Ctrl+Enter submits unlock", async () => {538 await setupVault(ctx);539 const newPage = await ctx.newPage();540 const extId = await waitForSW(ctx);541 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);542 await newPage.waitForTimeout(2000);543 await newPage.locator("#master-password").fill("password123");544 await newPage.keyboard.press("Control+Enter");545 await newPage.waitForTimeout(2000);546 const entries = newPage.locator("[class*=entry]");547 await expect(entries.first()).toBeVisible({ timeout: 5000 });548 await ctx.close();549 });550551 test("KBL03 - Password field accepts paste", async () => {552 await setupVault(ctx);553 const newPage = await ctx.newPage();554 const extId = await waitForSW(ctx);555 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);556 await newPage.waitForTimeout(2000);557 await newPage.locator("#master-password").fill("password123");558 await newPage.keyboard.press("Control+a");559 await newPage.keyboard.press("Control+c");560 await newPage.locator("#master-password").fill("");561 await newPage.locator("#master-password").focus();562 await newPage.keyboard.press("Control+v");563 await newPage.waitForTimeout(200);564 const value = await newPage.locator("#master-password").inputValue();565 expect(value).toBe("password123");566 await ctx.close();567 });568569 test("KBL04 - Show password toggle via click", async () => {570 const { popup: _popup } = await setupVault(ctx);571 const newPage = await ctx.newPage();572 const extId = await waitForSW(ctx);573 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);574 await newPage.waitForTimeout(2000);575 const showBtn = newPage.locator("[aria-label*='show' i], [aria-label*='reveal' i]").first();576 if (await showBtn.isVisible({ timeout: 2000 }).catch(() => false)) {577 await showBtn.click();578 await newPage.waitForTimeout(200);579 const inputType = await newPage.locator("#master-password").getAttribute("type");580 expect(inputType).toBe("text");581 }582 await ctx.close();583 });584585 test("KBL05 - Shift+Tab navigates backwards on lock", async () => {586 const { popup: _popup } = await setupVault(ctx);587 const newPage = await ctx.newPage();588 const extId = await waitForSW(ctx);589 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);590 await newPage.waitForTimeout(2000);591 await newPage.keyboard.press("Shift+Tab");592 await newPage.waitForTimeout(100);593 const focused = await newPage.evaluate(() => document.activeElement?.tagName);594 expect(focused).toBeTruthy();595 await ctx.close();596 });597});598599test.describe("Edit Entry Keyboard", () => {600 let ctx: BrowserContext;601 test.afterEach(async () => { await ctx.close(); });602 test.beforeEach(async () => { ctx = await makeContext(); });603604 test("KBD01 - Tab navigates edit form fields", async () => {605 const { popup } = await setupVault(ctx);606 await popup.locator('input[placeholder="search vault..."]').click();607 await popup.getByRole("button", { name: /new entry/i }).click();608 await popup.locator(".peach-new-name-input").fill("EditKB");609 await popup.locator('input[placeholder="domain.com"]').fill("https://editkb.com");610 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");611 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");612 await popup.getByRole("button", { name: /create/i }).click();613 await popup.waitForTimeout(1500);614 await popup.locator(".peach-entry-name-button").first().click();615 await popup.waitForTimeout(500);616 await popup.locator("button[aria-label='Edit']").first().click();617 await popup.waitForTimeout(500);618 await popup.keyboard.press("Tab");619 await popup.waitForTimeout(100);620 const focused = await popup.evaluate(() => document.activeElement?.tagName);621 expect(focused).toBe("INPUT");622 await ctx.close();623 });624625 test("KBD02 - Enter saves edit", async () => {626 const { popup } = await setupVault(ctx);627 await popup.locator('input[placeholder="search vault..."]').click();628 await popup.getByRole("button", { name: /new entry/i }).click();629 await popup.locator(".peach-new-name-input").fill("EditSaveKB");630 await popup.locator('input[placeholder="domain.com"]').fill("https://editsavekb.com");631 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");632 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");633 await popup.getByRole("button", { name: /create/i }).click();634 await popup.waitForTimeout(1500);635 await popup.locator(".peach-entry-name-button").first().click();636 await popup.waitForTimeout(500);637 await popup.locator("button[aria-label='Edit']").first().click();638 await popup.waitForTimeout(500);639 await popup.locator(".peach-new-name-input").fill("EditSaveKBChanged");640 await popup.keyboard.press("Enter");641 await popup.waitForTimeout(1500);642 await expect(popup.locator("text=EditSaveKBChanged")).toBeVisible({ timeout: 3000 });643 await ctx.close();644 });645646 test("KBD03 - Escape cancels edit", async () => {647 const { popup } = await setupVault(ctx);648 await popup.locator('input[placeholder="search vault..."]').click();649 await popup.getByRole("button", { name: /new entry/i }).click();650 await popup.locator(".peach-new-name-input").fill("CancelEditKB");651 await popup.locator('input[placeholder="domain.com"]').fill("https://cancelkb.com");652 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");653 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");654 await popup.getByRole("button", { name: /create/i }).click();655 await popup.waitForTimeout(1500);656 await popup.locator(".peach-entry-name-button").first().click();657 await popup.waitForTimeout(500);658 await popup.locator("button[aria-label='Edit']").first().click();659 await popup.waitForTimeout(500);660 await popup.locator(".peach-new-name-input").fill("ShouldNotChange");661 await popup.keyboard.press("Escape");662 await popup.waitForTimeout(300);663 await expect(popup.locator("text=CancelEditKB")).toBeVisible({ timeout: 3000 });664 await ctx.close();665 });666});667668test.describe("Search Field Keyboard", () => {669 let ctx: BrowserContext;670 test.afterEach(async () => { await ctx.close(); });671 test.beforeEach(async () => { ctx = await makeContext(); });672673 test("KBS01 - Typing filters results immediately", async () => {674 const { popup } = await setupVault(ctx);675 for (const name of ["FilterTest1", "FilterTest2", "OtherEntry"]) {676 await popup.locator('input[placeholder="search vault..."]').click();677 await popup.getByRole("button", { name: /new entry/i }).click();678 await popup.locator(".peach-new-name-input").fill(name);679 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);680 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);681 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");682 await popup.getByRole("button", { name: /create/i }).click();683 await popup.waitForTimeout(800);684 }685 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();686 await searchInput.click();687 await searchInput.fill("Filter");688 await popup.waitForTimeout(300);689 await expect(popup.locator("text=FilterTest1")).toBeVisible();690 await expect(popup.locator("text=OtherEntry")).not.toBeVisible();691 await ctx.close();692 });693694 test("KBS02 - Backspace reduces search query", async () => {695 const { popup } = await setupVault(ctx);696 await popup.locator('input[placeholder="search vault..."]').click();697 await popup.getByRole("button", { name: /new entry/i }).click();698 await popup.locator(".peach-new-name-input").fill("BackspaceTest");699 await popup.locator('input[placeholder="domain.com"]').fill("https://back.com");700 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");701 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");702 await popup.getByRole("button", { name: /create/i }).click();703 await popup.waitForTimeout(1500);704 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();705 await searchInput.click();706 await searchInput.fill("BackspaceTest");707 await popup.keyboard.press("Backspace");708 await popup.keyboard.press("Backspace");709 await popup.keyboard.press("Backspace");710 await popup.keyboard.press("Backspace");711 await popup.keyboard.press("Backspace");712 await popup.keyboard.press("Backspace");713 await popup.keyboard.press("Backspace");714 await popup.keyboard.press("Backspace");715 await popup.keyboard.press("Backspace");716 await popup.keyboard.press("Backspace");717 await popup.keyboard.press("Backspace");718 await popup.waitForTimeout(200);719 await ctx.close();720 });721722 test("KBS03 - Clear search shows all entries", async () => {723 const { popup } = await setupVault(ctx);724 for (const name of ["ShowAll1", "ShowAll2"]) {725 await popup.locator('input[placeholder="search vault..."]').click();726 await popup.getByRole("button", { name: /new entry/i }).click();727 await popup.locator(".peach-new-name-input").fill(name);728 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);729 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);730 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");731 await popup.getByRole("button", { name: /create/i }).click();732 await popup.waitForTimeout(800);733 }734 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();735 await searchInput.click();736 await searchInput.fill("ShowAll1");737 await popup.waitForTimeout(300);738 await searchInput.fill("");739 await popup.waitForTimeout(300);740 await expect(popup.locator("text=ShowAll1")).toBeVisible();741 await expect(popup.locator("text=ShowAll2")).toBeVisible();742 await ctx.close();743 });744745 test("KBS04 - Search with no results shows empty state", async () => {746 const { popup } = await setupVault(ctx);747 await popup.locator('input[placeholder="search vault..."]').click();748 await popup.getByRole("button", { name: /new entry/i }).click();749 await popup.locator(".peach-new-name-input").fill("SingleEntry");750 await popup.locator('input[placeholder="domain.com"]').fill("https://single.com");751 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");752 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");753 await popup.getByRole("button", { name: /create/i }).click();754 await popup.waitForTimeout(1500);755 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();756 await searchInput.click();757 await searchInput.fill("NothingMatches");758 await popup.waitForTimeout(500);759 const empty = popup.locator("text=no entries, text=no results, text=0 results");760 const hasEmpty = await empty.isVisible({ timeout: 2000 }).catch(() => false);761 expect(hasEmpty).toBeTruthy();762 await ctx.close();763 });764765 test("KBS05 - Search preserves cursor position", async () => {766 const { popup } = await setupVault(ctx);767 await popup.locator('input[placeholder="search vault..."]').click();768 await popup.getByRole("button", { name: /new entry/i }).click();769 await popup.locator(".peach-new-name-input").fill("CursorPreserve");770 await popup.locator('input[placeholder="domain.com"]').fill("https://cursor.com");771 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");772 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");773 await popup.getByRole("button", { name: /create/i }).click();774 await popup.waitForTimeout(1500);775 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();776 await searchInput.click();777 await searchInput.fill("Cursor");778 await popup.keyboard.press("ArrowLeft");779 await popup.keyboard.press("ArrowLeft");780 await popup.waitForTimeout(100);781 await popup.keyboard.press("X");782 await popup.waitForTimeout(100);783 const value = await searchInput.inputValue();784 expect(value).toBe("CurXor");785 await ctx.close();786 });787});