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-af-${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("Content Script Injection", () => {48 let ctx: BrowserContext;49 test.afterEach(async () => { await ctx.close(); });50 test.beforeEach(async () => { ctx = await makeContext(); });5152 test("AF01 - icon appears on page with password field", async () => {53 const { popup: _popup } = await setupVault(ctx);54 const page = await ctx.newPage();55 await page.setContent(`56 <html><body>57 <form><input type="password" id="p"></form>58 </body></html>59 `);60 await page.waitForTimeout(3000);61 const icon = page.locator("[data-peach-autofill]");62 const hasIcon = await icon.isVisible({ timeout: 5000 }).catch(() => false);63 expect(hasIcon).toBeTruthy();64 await ctx.close();65 });6667 test("AF02 - icon does not appear on page without password fields", async () => {68 const { popup: _popup } = await setupVault(ctx);69 const page = await ctx.newPage();70 await page.setContent(`<html><body><input type="text" id="t"></body></html>`);71 await page.waitForTimeout(2000);72 const icon = page.locator("[data-peach-autofill]");73 const hasIcon = await icon.count();74 expect(hasIcon).toBe(0);75 await ctx.close();76 });7778 test("AF03 - icon appears on login forms with both username and password", async () => {79 const { popup: _popup } = await setupVault(ctx);80 const page = await ctx.newPage();81 await page.setContent(`82 <html><body>83 <form>84 <input type="text" name="u" autocomplete="username">85 <input type="password" name="p" autocomplete="current-password">86 </form>87 </body></html>88 `);89 await page.waitForTimeout(3000);90 const icon = page.locator("[data-peach-autofill]");91 const hasIcon = await icon.isVisible({ timeout: 5000 }).catch(() => false);92 expect(hasIcon).toBeTruthy();93 await ctx.close();94 });9596 test("AF04 - multiple password fields shows single icon", async () => {97 const { popup: _popup } = await setupVault(ctx);98 const page = await ctx.newPage();99 await page.setContent(`100 <html><body>101 <form>102 <input type="password" name="old">103 <input type="password" name="new">104 <input type="password" name="confirm">105 </form>106 </body></html>107 `);108 await page.waitForTimeout(3000);109 const icons = page.locator("[data-peach-autofill]");110 const count = await icons.count();111 expect(count).toBeLessThanOrEqual(1);112 await ctx.close();113 });114115 test("AF05 - icon respects display:none parent elements", async () => {116 const { popup: _popup } = await setupVault(ctx);117 const page = await ctx.newPage();118 await page.setContent(`119 <html><body>120 <div style="display:none">121 <input type="password" id="p">122 </div>123 </body></html>124 `);125 await page.waitForTimeout(2000);126 const icon = page.locator("[data-peach-autofill]");127 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);128 expect(hasIcon).toBeFalsy();129 await ctx.close();130 });131132 test("AF06 - icon respects visibility:hidden parent elements", async () => {133 const { popup: _popup } = await setupVault(ctx);134 const page = await ctx.newPage();135 await page.setContent(`136 <html><body>137 <div style="visibility:hidden">138 <input type="password" id="p">139 </div>140 </body></html>141 `);142 await page.waitForTimeout(2000);143 const icon = page.locator("[data-peach-autofill]");144 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);145 expect(hasIcon).toBeFalsy();146 await ctx.close();147 });148});149150test.describe("Autofill Trigger Behavior", () => {151 let ctx: BrowserContext;152 test.afterEach(async () => { await ctx.close(); });153 test.beforeEach(async () => { ctx = await makeContext(); });154155 test("AF07 - click on icon opens credential selector", async () => {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 await popup.locator(".peach-new-name-input").fill("AutoFillTest");160 await popup.locator('input[placeholder="domain.com"]').fill("https://autofill.com");161 await popup.locator('input[placeholder="username or email"]').fill("autouser");162 await popup.locator(".peach-new-field-input[placeholder='password']").fill("autopass");163 await popup.getByRole("button", { name: /create/i }).click();164 await popup.waitForTimeout(1500);165 const page = await ctx.newPage();166 await page.setContent(`167 <html><body>168 <form><input type="text" id="u" autocomplete="username">169 <input type="password" id="p" autocomplete="current-password"></form>170 </body></html>171 `);172 await page.waitForTimeout(3000);173 const icon = page.locator("[data-peach-autofill]");174 if (await icon.isVisible({ timeout: 3000 }).catch(() => false)) {175 await icon.click();176 await page.waitForTimeout(500);177 }178 const popup2 = await ctx.newPage();179 await popup2.close();180 await ctx.close();181 });182183 test("AF08 - no autofill without user gesture", async () => {184 const { popup } = await setupVault(ctx);185 await popup.locator('input[placeholder="search vault..."]').click();186 await popup.getByRole("button", { name: /new entry/i }).click();187 await popup.locator(".peach-new-name-input").fill("NoGestureTest");188 await popup.locator('input[placeholder="domain.com"]').fill("https://nogesture.com");189 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");190 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");191 await popup.getByRole("button", { name: /create/i }).click();192 await popup.waitForTimeout(1500);193 const page = await ctx.newPage();194 await page.setContent(`<html><body><input type="password" id="p"></body></html>`);195 await page.waitForTimeout(3000);196 const input = page.locator("#p");197 const value = await input.inputValue();198 expect(value).toBe("");199 await ctx.close();200 });201202 test("AF09 - autofill matches entry by domain", async () => {203 const { popup } = await setupVault(ctx);204 await popup.locator('input[placeholder="search vault..."]').click();205 await popup.getByRole("button", { name: /new entry/i }).click();206 await popup.locator(".peach-new-name-input").fill("GitHub");207 await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");208 await popup.locator('input[placeholder="username or email"]').fill("testuser");209 await popup.locator(".peach-new-field-input[placeholder='password']").fill("secretpass");210 await popup.getByRole("button", { name: /create/i }).click();211 await popup.waitForTimeout(1500);212 const page = await ctx.newPage();213 await page.setContent(`214 <html><body>215 <form action="https://github.com/login" method="post">216 <input type="text" name="login" autocomplete="username">217 <input type="password" name="password" autocomplete="current-password">218 </form>219 </body></html>220 `);221 await page.waitForTimeout(3000);222 await ctx.close();223 });224225 test("AF10 - autofill requires exact domain match", async () => {226 const { popup } = await setupVault(ctx);227 await popup.locator('input[placeholder="search vault..."]').click();228 await popup.getByRole("button", { name: /new entry/i }).click();229 await popup.locator(".peach-new-name-input").fill("BankEntry");230 await popup.locator('input[placeholder="domain.com"]').fill("https://mybank.com");231 await popup.locator('input[placeholder="username or email"]').fill("bankuser");232 await popup.locator(".peach-new-field-input[placeholder='password']").fill("bankpass");233 await popup.getByRole("button", { name: /create/i }).click();234 await popup.waitForTimeout(1500);235 const page = await ctx.newPage();236 await page.setContent(`237 <html><body>238 <form>239 <input type="text" autocomplete="username">240 <input type="password" autocomplete="current-password">241 </form>242 </body></html>243 `);244 await page.waitForTimeout(2000);245 const inputs = page.locator("input");246 for (let i = 0; i < await inputs.count(); i++) {247 const val = await inputs.nth(i).inputValue();248 expect(val).not.toMatch(/bankuser|bankpass/);249 }250 await ctx.close();251 });252253 test("AF11 - autofill popup shows matching entries", async () => {254 const { popup } = await setupVault(ctx);255 await popup.locator('input[placeholder="search vault..."]').click();256 await popup.getByRole("button", { name: /new entry/i }).click();257 await popup.locator(".peach-new-name-input").fill("GmailLogin");258 await popup.locator('input[placeholder="domain.com"]').fill("https://accounts.google.com");259 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");260 await popup.locator(".peach-new-field-input[placeholder='password']").fill("gmailpass");261 await popup.getByRole("button", { name: /create/i }).click();262 await popup.waitForTimeout(1500);263 const page = await ctx.newPage();264 await page.setContent(`265 <html><body>266 <form>267 <input type="email" autocomplete="username">268 <input type="password" autocomplete="current-password">269 </form>270 </body></html>271 `);272 await page.waitForTimeout(3000);273 await ctx.close();274 });275276 test("AF12 - autofill dismisses on escape", async () => {277 const { popup } = await setupVault(ctx);278 await popup.locator('input[placeholder="search vault..."]').click();279 await popup.getByRole("button", { name: /new entry/i }).click();280 await popup.locator(".peach-new-name-input").fill("EscapeTest");281 await popup.locator('input[placeholder="domain.com"]').fill("https://escape.com");282 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");283 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");284 await popup.getByRole("button", { name: /create/i }).click();285 await popup.waitForTimeout(1500);286 const page = await ctx.newPage();287 await page.setContent(`288 <html><body>289 <form><input type="password" id="p"></form>290 </body></html>291 `);292 await page.waitForTimeout(3000);293 await ctx.close();294 });295});296297test.describe("Form Detection Edge Cases", () => {298 let ctx: BrowserContext;299 test.afterEach(async () => { await ctx.close(); });300 test.beforeEach(async () => { ctx = await makeContext(); });301302 test("AF13 - detects password field in iframe", async () => {303 const { popup: _popup } = await setupVault(ctx);304 const page = await ctx.newPage();305 await page.setContent(`306 <html><body>307 <iframe srcdoc="<input type='password' id='p'>"></iframe>308 </body></html>309 `);310 await page.waitForTimeout(3000);311 await ctx.close();312 });313314 test("AF14 - detects dynamically added password fields", async () => {315 const { popup: _popup } = await setupVault(ctx);316 const page = await ctx.newPage();317 await page.setContent(`<html><body><form id="f"></form></body></html>`);318 await page.waitForTimeout(2000);319 await page.evaluate(() => {320 const input = document.createElement("input");321 input.type = "password";322 input.id = "p";323 document.getElementById("f")?.appendChild(input);324 });325 await page.waitForTimeout(2000);326 await ctx.close();327 });328329 test("AF15 - handles shadow DOM correctly", async () => {330 const { popup: _popup } = await setupVault(ctx);331 const page = await ctx.newPage();332 await page.setContent(`<html><body></body></html>`);333 await page.waitForTimeout(1000);334 await ctx.close();335 });336337 test("AF16 - ignores password fields in non-login forms", async () => {338 const { popup: _popup } = await setupVault(ctx);339 const page = await ctx.newPage();340 await page.setContent(`341 <html><body>342 <form action="/search">343 <input type="text" name="q">344 <input type="password" name="sensitive">345 </form>346 </body></html>347 `);348 await page.waitForTimeout(2000);349 await ctx.close();350 });351352 test("AF17 - handles cross-origin iframes gracefully", async () => {353 const { popup: _popup } = await setupVault(ctx);354 const page = await ctx.newPage();355 await page.setContent(`<html><body></body></html>`);356 await page.waitForTimeout(1000);357 await ctx.close();358 });359360 test("AF18 - handles password manager popups correctly", async () => {361 const { popup: _popup } = await setupVault(ctx);362 const page = await ctx.newPage();363 await page.setContent(`364 <html><body>365 <form>366 <input type="text" autocomplete="username">367 <input type="password" autocomplete="current-password">368 </form>369 </body></html>370 `);371 await page.waitForTimeout(2000);372 await ctx.close();373 });374375 test("AF19 - icon appears for new password input type", async () => {376 const { popup: _popup } = await setupVault(ctx);377 const page = await ctx.newPage();378 await page.setContent(`<html><body><input type="password"></body></html>`);379 await page.waitForTimeout(3000);380 const icon = page.locator("[data-peach-autofill]");381 const hasIcon = await icon.isVisible({ timeout: 3000 }).catch(() => false);382 expect(hasIcon).toBeTruthy();383 await ctx.close();384 });385386 test("AF20 - autofill respects autocomplete=off on password field", async () => {387 const { popup: _popup } = await setupVault(ctx);388 const page = await ctx.newPage();389 await page.setContent(`390 <html><body>391 <input type="text" autocomplete="username">392 <input type="password" autocomplete="off">393 </body></html>394 `);395 await page.waitForTimeout(3000);396 const icon = page.locator("[data-peach-autofill]");397 const hasIcon = await icon.count();398 expect(hasIcon).toBe(0);399 await ctx.close();400 });401402 test("AF21 - handles multiple forms on same page", async () => {403 const { popup: _popup } = await setupVault(ctx);404 const page = await ctx.newPage();405 await page.setContent(`406 <html><body>407 <form id="f1"><input type="password"></form>408 <form id="f2"><input type="password"></form>409 <form id="f3"><input type="password"></form>410 </body></html>411 `);412 await page.waitForTimeout(3000);413 const icons = page.locator("[data-peach-autofill]");414 const count = await icons.count();415 expect(count).toBeLessThanOrEqual(3);416 await ctx.close();417 });418});419420test.describe("Credential Matching Logic", () => {421 let ctx: BrowserContext;422 test.afterEach(async () => { await ctx.close(); });423 test.beforeEach(async () => { ctx = await makeContext(); });424425 test("AF22 - matches subdomain entries", async () => {426 const { popup } = await setupVault(ctx);427 await popup.locator('input[placeholder="search vault..."]').click();428 await popup.getByRole("button", { name: /new entry/i }).click();429 await popup.locator(".peach-new-name-input").fill("AWSConsole");430 await popup.locator('input[placeholder="domain.com"]').fill("https://aws.amazon.com");431 await popup.locator('input[placeholder="username or email"]').fill("admin");432 await popup.locator(".peach-new-field-input[placeholder='password']").fill("awssecret");433 await popup.getByRole("button", { name: /create/i }).click();434 await popup.waitForTimeout(1500);435 const page = await ctx.newPage();436 await page.setContent(`437 <html><body>438 <form action="https://signin.aws.amazon.com/login">439 <input type="text" name="username">440 <input type="password" name="password">441 </form>442 </body></html>443 `);444 await page.waitForTimeout(3000);445 await ctx.close();446 });447448 test("AF23 - matches base domain when subdomain is different", async () => {449 const { popup } = await setupVault(ctx);450 await popup.locator('input[placeholder="search vault..."]').click();451 await popup.getByRole("button", { name: /new entry/i }).click();452 await popup.locator(".peach-new-name-input").fill("GoogleDrive");453 await popup.locator('input[placeholder="domain.com"]').fill("https://drive.google.com");454 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");455 await popup.locator(".peach-new-field-input[placeholder='password']").fill("gdrivepass");456 await popup.getByRole("button", { name: /create/i }).click();457 await popup.waitForTimeout(1500);458 const page = await ctx.newPage();459 await page.setContent(`460 <html><body>461 <form action="https://accounts.google.com">462 <input type="text" name="Email">463 <input type="password" name="Passwd">464 </form>465 </body></html>466 `);467 await page.waitForTimeout(3000);468 await ctx.close();469 });470471 test("AF24 - best match selected when multiple entries match", async () => {472 const { popup } = await setupVault(ctx);473 for (const name of ["Twitter", "TwitterAlt"]) {474 await popup.locator('input[placeholder="search vault..."]').click();475 await popup.getByRole("button", { name: /new entry/i }).click();476 await popup.locator(".peach-new-name-input").fill(name);477 await popup.locator('input[placeholder="domain.com"]').fill("https://twitter.com");478 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);479 await popup.locator(".peach-new-field-input[placeholder='password']").fill("twitterpass");480 await popup.getByRole("button", { name: /create/i }).click();481 await popup.waitForTimeout(800);482 }483 const page = await ctx.newPage();484 await page.setContent(`485 <html><body>486 <form>487 <input type="text" autocomplete="username">488 <input type="password" autocomplete="current-password">489 </form>490 </body></html>491 `);492 await page.waitForTimeout(3000);493 await ctx.close();494 });495496 test("AF25 - URL without protocol still matches", async () => {497 const { popup } = await setupVault(ctx);498 await popup.locator('input[placeholder="search vault..."]').click();499 await popup.getByRole("button", { name: /new entry/i }).click();500 await popup.locator(".peach-new-name-input").fill("NoProto");501 await popup.locator('input[placeholder="domain.com"]').fill("www.example.com");502 await popup.locator('input[placeholder="username or email"]').fill("noprotouser");503 await popup.locator(".peach-new-field-input[placeholder='password']").fill("nopass");504 await popup.getByRole("button", { name: /create/i }).click();505 await popup.waitForTimeout(1500);506 const page = await ctx.newPage();507 await page.setContent(`<html><body><form><input type="password"></form></body></html>`);508 await page.waitForTimeout(2000);509 await ctx.close();510 });511512 test("AF26 - entry with no URL matches any password field", async () => {513 const { popup } = await setupVault(ctx);514 await popup.locator('input[placeholder="search vault..."]').click();515 await popup.getByRole("button", { name: /new entry/i }).click();516 await popup.locator(".peach-new-name-input").fill("NoURLEntry");517 await popup.locator('input[placeholder="domain.com"]').fill("");518 await popup.locator('input[placeholder="username or email"]').fill("nourluser");519 await popup.locator(".peach-new-field-input[placeholder='password']").fill("nourlpass");520 await popup.getByRole("button", { name: /create/i }).click();521 await popup.waitForTimeout(1500);522 const page = await ctx.newPage();523 await page.setContent(`<html><body><form><input type="password"></form></body></html>`);524 await page.waitForTimeout(2000);525 await ctx.close();526 });527});528529test.describe("Autofill Security", () => {530 let ctx: BrowserContext;531 test.afterEach(async () => { await ctx.close(); });532 test.beforeEach(async () => { ctx = await makeContext(); });533534 test("AF27 - no autofill on HTTP pages for HTTPS entries", async () => {535 const { popup } = await setupVault(ctx);536 await popup.locator('input[placeholder="search vault..."]').click();537 await popup.getByRole("button", { name: /new entry/i }).click();538 await popup.locator(".peach-new-name-input").fill("SecureSite");539 await popup.locator('input[placeholder="domain.com"]').fill("https://secure.example.com");540 await popup.locator('input[placeholder="username or email"]').fill("secureuser");541 await popup.locator(".peach-new-field-input[placeholder='password']").fill("securepass");542 await popup.getByRole("button", { name: /create/i }).click();543 await popup.waitForTimeout(1500);544 const page = await ctx.newPage();545 await page.setContent(`546 <html><body>547 <form action="http://insecure.example.com/login">548 <input type="password">549 </form>550 </body></html>551 `);552 await page.waitForTimeout(2000);553 await ctx.close();554 });555556 test("AF28 - credential not stored in page DOM after autofill", async () => {557 const { popup } = await setupVault(ctx);558 await popup.locator('input[placeholder="search vault..."]').click();559 await popup.getByRole("button", { name: /new entry/i }).click();560 await popup.locator(".peach-new-name-input").fill("DOMSecurity");561 await popup.locator('input[placeholder="domain.com"]').fill("https://domsec.com");562 await popup.locator('input[placeholder="username or email"]').fill("domuser");563 await popup.locator(".peach-new-field-input[placeholder='password']").fill("dompass");564 await popup.getByRole("button", { name: /create/i }).click();565 await popup.waitForTimeout(1500);566 const page = await ctx.newPage();567 await page.setContent(`<html><body><form><input type="password" id="p"></form></body></html>`);568 await page.waitForTimeout(3000);569 await ctx.close();570 });571572 test("AF29 - autofill clears after form submission", async () => {573 const { popup } = await setupVault(ctx);574 await popup.locator('input[placeholder="search vault..."]').click();575 await popup.getByRole("button", { name: /new entry/i }).click();576 await popup.locator(".peach-new-name-input").fill("FormSubmit");577 await popup.locator('input[placeholder="domain.com"]').fill("https://submit.com");578 await popup.locator('input[placeholder="username or email"]').fill("submituser");579 await popup.locator(".peach-new-field-input[placeholder='password']").fill("submitpass");580 await popup.getByRole("button", { name: /create/i }).click();581 await popup.waitForTimeout(1500);582 const page = await ctx.newPage();583 await page.setContent(`584 <html><body>585 <form>586 <input type="text" id="u">587 <input type="password" id="p">588 <button type="submit">Submit</button>589 </form>590 </body></html>591 `);592 await page.waitForTimeout(3000);593 await ctx.close();594 });595596 test("AF30 - sensitive fields not logged to console", async () => {597 const { popup: _popup } = await setupVault(ctx);598 const page = await ctx.newPage();599 const errors: string[] = [];600 page.on("console", msg => {601 if (msg.type() === "error") errors.push(msg.text());602 });603 await page.setContent(`<html><body><input type="password" id="p"></body></html>`);604 await page.waitForTimeout(2000);605 const passLogged = errors.some(e => e.includes("password") || e.includes("pass"));606 expect(passLogged).toBeFalsy();607 await ctx.close();608 });609610 test("AF31 - autofill does not fire on readonly fields", async () => {611 const { popup: _popup } = await setupVault(ctx);612 const page = await ctx.newPage();613 await page.setContent(`<html><body><input type="password" readonly></body></html>`);614 await page.waitForTimeout(3000);615 const icon = page.locator("[data-peach-autofill]");616 const hasIcon = await icon.isVisible({ timeout: 3000 }).catch(() => false);617 expect(hasIcon).toBeFalsy();618 await ctx.close();619 });620621 test("AF32 - autofill does not fire on disabled fields", async () => {622 const { popup: _popup } = await setupVault(ctx);623 const page = await ctx.newPage();624 await page.setContent(`<html><body><input type="password" disabled></body></html>`);625 await page.waitForTimeout(3000);626 const icon = page.locator("[data-peach-autofill]");627 const hasIcon = await icon.isVisible({ timeout: 3000 }).catch(() => false);628 expect(hasIcon).toBeFalsy();629 await ctx.close();630 });631});