keyboard-comprehensive.spec.ts

35.4 KB · raw

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});