autofill-deep.spec.ts

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