import { test, expect, BrowserContext } from "@playwright/test"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import os from "os"; import fs from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const EXTENSION_PATH = join(__dirname, "..", "dist"); async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-af-${Date.now()}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise(r => setTimeout(r, 500)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No SW after ${maxWait}ms`); return new URL(sw.url()).host; } async function setupVault(ctx: BrowserContext): Promise<{ popup: any, extId: string }> { const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(3000); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill("password123"); await popup.locator("#confirm-password").fill("password123"); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup, extId }; } test.describe("Content Script Injection", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("AF01 - icon appears on page with password field", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 5000 }).catch(() => false); expect(hasIcon).toBeTruthy(); await ctx.close(); }); test("AF02 - icon does not appear on page without password fields", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(2000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.count(); expect(hasIcon).toBe(0); await ctx.close(); }); test("AF03 - icon appears on login forms with both username and password", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 5000 }).catch(() => false); expect(hasIcon).toBeTruthy(); await ctx.close(); }); test("AF04 - multiple password fields shows single icon", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); const icons = page.locator("[data-peach-autofill]"); const count = await icons.count(); expect(count).toBeLessThanOrEqual(1); await ctx.close(); }); test("AF05 - icon respects display:none parent elements", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false); expect(hasIcon).toBeFalsy(); await ctx.close(); }); test("AF06 - icon respects visibility:hidden parent elements", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false); expect(hasIcon).toBeFalsy(); await ctx.close(); }); }); test.describe("Autofill Trigger Behavior", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("AF07 - click on icon opens credential selector", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("AutoFillTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://autofill.com"); await popup.locator('input[placeholder="username or email"]').fill("autouser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("autopass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); if (await icon.isVisible({ timeout: 3000 }).catch(() => false)) { await icon.click(); await page.waitForTimeout(500); } const popup2 = await ctx.newPage(); await popup2.close(); await ctx.close(); }); test("AF08 - no autofill without user gesture", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("NoGestureTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://nogesture.com"); await popup.locator('input[placeholder="username or email"]').fill("user@test.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(3000); const input = page.locator("#p"); const value = await input.inputValue(); expect(value).toBe(""); await ctx.close(); }); test("AF09 - autofill matches entry by domain", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("GitHub"); await popup.locator('input[placeholder="domain.com"]').fill("https://github.com"); await popup.locator('input[placeholder="username or email"]').fill("testuser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("secretpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF10 - autofill requires exact domain match", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("BankEntry"); await popup.locator('input[placeholder="domain.com"]').fill("https://mybank.com"); await popup.locator('input[placeholder="username or email"]').fill("bankuser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("bankpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); const inputs = page.locator("input"); for (let i = 0; i < await inputs.count(); i++) { const val = await inputs.nth(i).inputValue(); expect(val).not.toMatch(/bankuser|bankpass/); } await ctx.close(); }); test("AF11 - autofill popup shows matching entries", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("GmailLogin"); await popup.locator('input[placeholder="domain.com"]').fill("https://accounts.google.com"); await popup.locator('input[placeholder="username or email"]').fill("user@gmail.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("gmailpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF12 - autofill dismisses on escape", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("EscapeTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://escape.com"); await popup.locator('input[placeholder="username or email"]').fill("user@escape.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); }); test.describe("Form Detection Edge Cases", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("AF13 - detects password field in iframe", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(` `); await page.waitForTimeout(3000); await ctx.close(); }); test("AF14 - detects dynamically added password fields", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); await page.evaluate(() => { const input = document.createElement("input"); input.type = "password"; input.id = "p"; document.getElementById("f")?.appendChild(input); }); await page.waitForTimeout(2000); await ctx.close(); }); test("AF15 - handles shadow DOM correctly", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(1000); await ctx.close(); }); test("AF16 - ignores password fields in non-login forms", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); await ctx.close(); }); test("AF17 - handles cross-origin iframes gracefully", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(1000); await ctx.close(); }); test("AF18 - handles password manager popups correctly", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); await ctx.close(); }); test("AF19 - icon appears for new password input type", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 3000 }).catch(() => false); expect(hasIcon).toBeTruthy(); await ctx.close(); }); test("AF20 - autofill respects autocomplete=off on password field", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(` `); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.count(); expect(hasIcon).toBe(0); await ctx.close(); }); test("AF21 - handles multiple forms on same page", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); const icons = page.locator("[data-peach-autofill]"); const count = await icons.count(); expect(count).toBeLessThanOrEqual(3); await ctx.close(); }); }); test.describe("Credential Matching Logic", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("AF22 - matches subdomain entries", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("AWSConsole"); await popup.locator('input[placeholder="domain.com"]').fill("https://aws.amazon.com"); await popup.locator('input[placeholder="username or email"]').fill("admin"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("awssecret"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF23 - matches base domain when subdomain is different", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("GoogleDrive"); await popup.locator('input[placeholder="domain.com"]').fill("https://drive.google.com"); await popup.locator('input[placeholder="username or email"]').fill("user@gmail.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("gdrivepass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF24 - best match selected when multiple entries match", async () => { const { popup } = await setupVault(ctx); for (const name of ["Twitter", "TwitterAlt"]) { await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill(name); await popup.locator('input[placeholder="domain.com"]').fill("https://twitter.com"); await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`); await popup.locator(".peach-new-field-input[placeholder='password']").fill("twitterpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(800); } const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF25 - URL without protocol still matches", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("NoProto"); await popup.locator('input[placeholder="domain.com"]').fill("www.example.com"); await popup.locator('input[placeholder="username or email"]').fill("noprotouser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("nopass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); await ctx.close(); }); test("AF26 - entry with no URL matches any password field", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("NoURLEntry"); await popup.locator('input[placeholder="domain.com"]').fill(""); await popup.locator('input[placeholder="username or email"]').fill("nourluser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("nourlpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); await ctx.close(); }); }); test.describe("Autofill Security", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("AF27 - no autofill on HTTP pages for HTTPS entries", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("SecureSite"); await popup.locator('input[placeholder="domain.com"]').fill("https://secure.example.com"); await popup.locator('input[placeholder="username or email"]').fill("secureuser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("securepass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(2000); await ctx.close(); }); test("AF28 - credential not stored in page DOM after autofill", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("DOMSecurity"); await popup.locator('input[placeholder="domain.com"]').fill("https://domsec.com"); await popup.locator('input[placeholder="username or email"]').fill("domuser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("dompass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF29 - autofill clears after form submission", async () => { const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("FormSubmit"); await popup.locator('input[placeholder="domain.com"]').fill("https://submit.com"); await popup.locator('input[placeholder="username or email"]').fill("submituser"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("submitpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const page = await ctx.newPage(); await page.setContent(`
`); await page.waitForTimeout(3000); await ctx.close(); }); test("AF30 - sensitive fields not logged to console", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); const errors: string[] = []; page.on("console", msg => { if (msg.type() === "error") errors.push(msg.text()); }); await page.setContent(``); await page.waitForTimeout(2000); const passLogged = errors.some(e => e.includes("password") || e.includes("pass")); expect(passLogged).toBeFalsy(); await ctx.close(); }); test("AF31 - autofill does not fire on readonly fields", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 3000 }).catch(() => false); expect(hasIcon).toBeFalsy(); await ctx.close(); }); test("AF32 - autofill does not fire on disabled fields", async () => { const { popup: _popup } = await setupVault(ctx); const page = await ctx.newPage(); await page.setContent(``); await page.waitForTimeout(3000); const icon = page.locator("[data-peach-autofill]"); const hasIcon = await icon.isVisible({ timeout: 3000 }).catch(() => false); expect(hasIcon).toBeFalsy(); await ctx.close(); }); });