keyboard-edge.spec.ts

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