totp-edge.spec.ts

26.4 KB · raw

1import { test, expect, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");1011async function makeContext() {12  const { chromium } = await import("playwright");13  const userDataDir = join(os.tmpdir(), `peach-totp-${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("TOTP Base32 Parsing", () => {48  let ctx: BrowserContext;49  test.afterEach(async () => { await ctx.close(); });50  test.beforeEach(async () => { ctx = await makeContext(); });5152  test("TOTP01 - standard base32 secret accepted", async () => {53    const { popup } = await setupVault(ctx);54    await popup.locator('input[placeholder="search vault..."]').click();55    await popup.getByRole("button", { name: /new entry/i }).click();56    await popup.locator(".peach-new-name-input").fill("Base32Std");57    await popup.locator('input[placeholder="domain.com"]').fill("https://base32.com");58    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");59    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");60    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");61    await popup.getByRole("button", { name: /create/i }).click();62    await popup.waitForTimeout(1500);63    await expect(popup.locator("text=Base32Std")).toBeVisible({ timeout: 3000 });64    await ctx.close();65  });6667  test("TOTP02 - base32 with spaces accepted", async () => {68    const { popup } = await setupVault(ctx);69    await popup.locator('input[placeholder="search vault..."]').click();70    await popup.getByRole("button", { name: /new entry/i }).click();71    await popup.locator(".peach-new-name-input").fill("Base32Space");72    await popup.locator('input[placeholder="domain.com"]').fill("https://space.com");73    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");74    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");75    await popup.locator("input[placeholder*='totp' i]").fill("JBSW Y3DP EHPK 3PXP");76    await popup.getByRole("button", { name: /create/i }).click();77    await popup.waitForTimeout(1500);78    await expect(popup.locator("text=Base32Space")).toBeVisible({ timeout: 3000 });79    await ctx.close();80  });8182  test("TOTP03 - base32 with lowercase accepted", async () => {83    const { popup } = await setupVault(ctx);84    await popup.locator('input[placeholder="search vault..."]').click();85    await popup.getByRole("button", { name: /new entry/i }).click();86    await popup.locator(".peach-new-name-input").fill("Base32Lower");87    await popup.locator('input[placeholder="domain.com"]').fill("https://lower.com");88    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");89    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");90    await popup.locator("input[placeholder*='totp' i]").fill("jbswy3dpehpk3pxp");91    await popup.getByRole("button", { name: /create/i }).click();92    await popup.waitForTimeout(1500);93    await expect(popup.locator("text=Base32Lower")).toBeVisible({ timeout: 3000 });94    await ctx.close();95  });9697  test("TOTP04 - invalid base32 shows error", async () => {98    const { popup } = await setupVault(ctx);99    await popup.locator('input[placeholder="search vault..."]').click();100    await popup.getByRole("button", { name: /new entry/i }).click();101    await popup.locator(".peach-new-name-input").fill("InvalidBase");102    await popup.locator('input[placeholder="domain.com"]').fill("https://invalid.com");103    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");104    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");105    await popup.locator("input[placeholder*='totp' i]").fill("INVALID!!!");106    await popup.getByRole("button", { name: /create/i }).click();107    await popup.waitForTimeout(500);108    const error = popup.locator(".peach-hint-error, .peach-error, text=invalid");109    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);110    expect(hasError).toBeTruthy();111    await ctx.close();112  });113114  test("TOTP05 - base32 with padding accepted", async () => {115    const { popup } = await setupVault(ctx);116    await popup.locator('input[placeholder="search vault..."]').click();117    await popup.getByRole("button", { name: /new entry/i }).click();118    await popup.locator(".peach-new-name-input").fill("Base32Pad");119    await popup.locator('input[placeholder="domain.com"]').fill("https://pad.com");120    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");121    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");122    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP====");123    await popup.getByRole("button", { name: /create/i }).click();124    await popup.waitForTimeout(1500);125    await expect(popup.locator("text=Base32Pad")).toBeVisible({ timeout: 3000 });126    await ctx.close();127  });128});129130test.describe("TOTP Countdown Accuracy", () => {131  let ctx: BrowserContext;132  test.afterEach(async () => { await ctx.close(); });133  test.beforeEach(async () => { ctx = await makeContext(); });134135  test("TOTP06 - countdown shows seconds remaining", async () => {136    const { popup } = await setupVault(ctx);137    await popup.locator('input[placeholder="search vault..."]').click();138    await popup.getByRole("button", { name: /new entry/i }).click();139    await popup.locator(".peach-new-name-input").fill("CountDown");140    await popup.locator('input[placeholder="domain.com"]').fill("https://count.com");141    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");142    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");143    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");144    await popup.getByRole("button", { name: /create/i }).click();145    await popup.waitForTimeout(1500);146    await popup.locator(".peach-entry-name-button").first().click();147    await popup.waitForTimeout(500);148    const countdown = popup.locator(".peach-totp-countdown");149    await expect(countdown).toBeVisible({ timeout: 3000 });150    const text = await countdown.textContent();151    expect(text).toMatch(/\d+s/);152    await ctx.close();153  });154155  test("TOTP07 - countdown decrements in real time", 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("Decrement");160    await popup.locator('input[placeholder="domain.com"]').fill("https://decrement.com");161    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");162    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");163    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");164    await popup.getByRole("button", { name: /create/i }).click();165    await popup.waitForTimeout(1500);166    await popup.locator(".peach-entry-name-button").first().click();167    await popup.waitForTimeout(500);168    const countdown = popup.locator(".peach-totp-countdown");169    const val1 = await countdown.textContent();170    await popup.waitForTimeout(2000);171    const val2 = await countdown.textContent();172    expect(parseInt(val2 ?? "0")).toBeLessThan(parseInt(val1 ?? "30"));173    await ctx.close();174  });175176  test("TOTP08 - ring progress animates with countdown", async () => {177    const { popup } = await setupVault(ctx);178    await popup.locator('input[placeholder="search vault..."]').click();179    await popup.getByRole("button", { name: /new entry/i }).click();180    await popup.locator(".peach-new-name-input").fill("RingAnim");181    await popup.locator('input[placeholder="domain.com"]').fill("https://ring.com");182    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");183    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");184    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");185    await popup.getByRole("button", { name: /create/i }).click();186    await popup.waitForTimeout(1500);187    await popup.locator(".peach-entry-name-button").first().click();188    await popup.waitForTimeout(500);189    const ring = popup.locator(".peach-totp-ring-progress");190    await expect(ring).toBeVisible({ timeout: 3000 });191    const dashOffset1 = await ring.getAttribute("stroke-dashoffset");192    await popup.waitForTimeout(2000);193    const dashOffset2 = await ring.getAttribute("stroke-dashoffset");194    expect(dashOffset1).not.toEqual(dashOffset2);195    await ctx.close();196  });197198  test("TOTP09 - code refreshes at 30 second boundary", async () => {199    const { popup } = await setupVault(ctx);200    await popup.locator('input[placeholder="search vault..."]').click();201    await popup.getByRole("button", { name: /new entry/i }).click();202    await popup.locator(".peach-new-name-input").fill("RefreshBound");203    await popup.locator('input[placeholder="domain.com"]').fill("https://refresh.com");204    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");205    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");206    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");207    await popup.getByRole("button", { name: /create/i }).click();208    await popup.waitForTimeout(1500);209    await popup.locator(".peach-entry-name-button").first().click();210    await popup.waitForTimeout(500);211    const code1 = await popup.locator(".peach-totp-code").textContent();212    const countdown = popup.locator(".peach-totp-countdown");213    const remaining = parseInt(await countdown.textContent() ?? "0");214    if (remaining > 5) {215      await popup.waitForTimeout(remaining + 2);216      const code2 = await popup.locator(".peach-totp-code").textContent();217      expect(code1).not.toEqual(code2);218    }219    await ctx.close();220  });221222  test("TOTP10 - 6 digit TOTP format correct", async () => {223    const { popup } = await setupVault(ctx);224    await popup.locator('input[placeholder="search vault..."]').click();225    await popup.getByRole("button", { name: /new entry/i }).click();226    await popup.locator(".peach-new-name-input").fill("SixDigit");227    await popup.locator('input[placeholder="domain.com"]').fill("https://six.com");228    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");229    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");230    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");231    await popup.getByRole("button", { name: /create/i }).click();232    await popup.waitForTimeout(1500);233    await popup.locator(".peach-entry-name-button").first().click();234    await popup.waitForTimeout(500);235    const code = await popup.locator(".peach-totp-code").textContent();236    const clean = code?.replace(/\s/g, "").replace(/[^0-9]/g, "");237    expect(clean?.length).toBe(6);238    await ctx.close();239  });240});241242test.describe("TOTP Copy Behavior", () => {243  let ctx: BrowserContext;244  test.afterEach(async () => { await ctx.close(); });245  test.beforeEach(async () => { ctx = await makeContext(); });246247  test("TOTP11 - copy button copies current code", async () => {248    const { popup } = await setupVault(ctx);249    await popup.locator('input[placeholder="search vault..."]').click();250    await popup.getByRole("button", { name: /new entry/i }).click();251    await popup.locator(".peach-new-name-input").fill("CopyTOTP");252    await popup.locator('input[placeholder="domain.com"]').fill("https://copytotp.com");253    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");254    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");255    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");256    await popup.getByRole("button", { name: /create/i }).click();257    await popup.waitForTimeout(1500);258    await popup.locator(".peach-entry-name-button").first().click();259    await popup.waitForTimeout(500);260    const copyBtn = popup.locator("[aria-label='Copy TOTP code']");261    if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {262      await copyBtn.click();263      await popup.waitForTimeout(200);264      const clipboard = await popup.evaluate(() => navigator.clipboard.readText());265      expect(clipboard).toMatch(/^\d{6}$/);266    }267    await ctx.close();268  });269270  test("TOTP12 - copy button shows feedback", async () => {271    const { popup } = await setupVault(ctx);272    await popup.locator('input[placeholder="search vault..."]').click();273    await popup.getByRole("button", { name: /new entry/i }).click();274    await popup.locator(".peach-new-name-input").fill("FeedbackTOTP");275    await popup.locator('input[placeholder="domain.com"]').fill("https://feedback.com");276    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");277    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");278    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");279    await popup.getByRole("button", { name: /create/i }).click();280    await popup.waitForTimeout(1500);281    await popup.locator(".peach-entry-name-button").first().click();282    await popup.waitForTimeout(500);283    const copyBtn = popup.locator("[aria-label='Copy TOTP code']");284    if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {285      await copyBtn.click();286      await popup.waitForTimeout(200);287      const isCopied = await copyBtn.evaluate((el: Element) => el.classList.contains("is-copied"));288      expect(isCopied).toBeTruthy();289    }290    await ctx.close();291  });292293  test("TOTP13 - copied code matches displayed code", async () => {294    const { popup } = await setupVault(ctx);295    await popup.locator('input[placeholder="search vault..."]').click();296    await popup.getByRole("button", { name: /new entry/i }).click();297    await popup.locator(".peach-new-name-input").fill("MatchCode");298    await popup.locator('input[placeholder="domain.com"]').fill("https://match.com");299    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");300    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");301    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");302    await popup.getByRole("button", { name: /create/i }).click();303    await popup.waitForTimeout(1500);304    await popup.locator(".peach-entry-name-button").first().click();305    await popup.waitForTimeout(500);306    const codeText = await popup.locator(".peach-totp-code").textContent();307    const copyBtn = popup.locator("[aria-label='Copy TOTP code']");308    if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {309      await copyBtn.click();310      await popup.waitForTimeout(200);311      const clipboard = await popup.evaluate(() => navigator.clipboard.readText());312      const cleanCode = codeText?.replace(/\s/g, "").replace(/[^0-9]/g, "");313      expect(clipboard).toBe(cleanCode);314    }315    await ctx.close();316  });317});318319test.describe("TOTP Edge Cases", () => {320  let ctx: BrowserContext;321  test.afterEach(async () => { await ctx.close(); });322  test.beforeEach(async () => { ctx = await makeContext(); });323324  test("TOTP14 - entry with empty TOTP shows no TOTP UI", async () => {325    const { popup } = await setupVault(ctx);326    await popup.locator('input[placeholder="search vault..."]').click();327    await popup.getByRole("button", { name: /new entry/i }).click();328    await popup.locator(".peach-new-name-input").fill("NoTOTPHere");329    await popup.locator('input[placeholder="domain.com"]').fill("https://nototp.com");330    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");331    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");332    await popup.getByRole("button", { name: /create/i }).click();333    await popup.waitForTimeout(1500);334    await popup.locator(".peach-entry-name-button").first().click();335    await popup.waitForTimeout(500);336    const totpBand = popup.locator(".peach-totp-band");337    const isHidden = await totpBand.isHidden().catch(() => true);338    expect(isHidden).toBe(true);339    await ctx.close();340  });341342  test("TOTP15 - TOTP survives vault lock/unlock", async () => {343    const { popup } = await setupVault(ctx);344    await popup.locator('input[placeholder="search vault..."]').click();345    await popup.getByRole("button", { name: /new entry/i }).click();346    await popup.locator(".peach-new-name-input").fill("LockTOTP");347    await popup.locator('input[placeholder="domain.com"]').fill("https://locktotp.com");348    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");349    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");350    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");351    await popup.getByRole("button", { name: /create/i }).click();352    await popup.waitForTimeout(1500);353    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");354    await lockBtn.click();355    await popup.waitForTimeout(1500);356    await popup.locator("#master-password").fill("password123");357    await popup.getByRole("button", { name: /unlock/i }).click();358    await popup.waitForTimeout(2000);359    await popup.locator(".peach-entry-name-button").first().click();360    await popup.waitForTimeout(500);361    const totpCode = popup.locator(".peach-totp-code");362    await expect(totpCode).toBeVisible({ timeout: 3000 });363    await ctx.close();364  });365366  test("TOTP16 - very long base32 secret accepted", async () => {367    const { popup } = await setupVault(ctx);368    await popup.locator('input[placeholder="search vault..."]').click();369    await popup.getByRole("button", { name: /new entry/i }).click();370    await popup.locator(".peach-new-name-input").fill("LongBase");371    await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");372    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");373    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");374    const longSecret = "JBSWY3DPEHPK3PXPEHPK3PXPJBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP";375    await popup.locator("input[placeholder*='totp' i]").fill(longSecret);376    await popup.getByRole("button", { name: /create/i }).click();377    await popup.waitForTimeout(1500);378    await expect(popup.locator("text=LongBase")).toBeVisible({ timeout: 3000 });379    await ctx.close();380  });381382  test("TOTP17 - base32 O and 0 treated correctly", async () => {383    const { popup } = await setupVault(ctx);384    await popup.locator('input[placeholder="search vault..."]').click();385    await popup.getByRole("button", { name: /new entry/i }).click();386    await popup.locator(".peach-new-name-input").fill("BaseOZero");387    await popup.locator('input[placeholder="domain.com"]').fill("https://ozero.com");388    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");389    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");390    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");391    await popup.getByRole("button", { name: /create/i }).click();392    await popup.waitForTimeout(1500);393    await expect(popup.locator("text=BaseOZero")).toBeVisible({ timeout: 3000 });394    await ctx.close();395  });396397  test("TOTP18 - base32 I and l treated correctly", async () => {398    const { popup } = await setupVault(ctx);399    await popup.locator('input[placeholder="search vault..."]').click();400    await popup.getByRole("button", { name: /new entry/i }).click();401    await popup.locator(".peach-new-name-input").fill("BaseIL");402    await popup.locator('input[placeholder="domain.com"]').fill("https://ilandl.com");403    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");404    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");405    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");406    await popup.getByRole("button", { name: /create/i }).click();407    await popup.waitForTimeout(1500);408    await expect(popup.locator("text=BaseIL")).toBeVisible({ timeout: 3000 });409    await ctx.close();410  });411});412413test.describe("TOTP Entry Management", () => {414  let ctx: BrowserContext;415  test.afterEach(async () => { await ctx.close(); });416  test.beforeEach(async () => { ctx = await makeContext(); });417418  test("TOTP19 - edit TOTP in existing entry", async () => {419    const { popup } = await setupVault(ctx);420    await popup.locator('input[placeholder="search vault..."]').click();421    await popup.getByRole("button", { name: /new entry/i }).click();422    await popup.locator(".peach-new-name-input").fill("EditTOTP");423    await popup.locator('input[placeholder="domain.com"]').fill("https://edittotp.com");424    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");425    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");426    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");427    await popup.getByRole("button", { name: /create/i }).click();428    await popup.waitForTimeout(1500);429    await popup.locator(".peach-entry-name-button").first().click();430    await popup.waitForTimeout(500);431    await popup.locator("button[aria-label='Edit']").first().click();432    await popup.waitForTimeout(500);433    const totpInput = popup.locator("input[placeholder*='totp' i]");434    await totpInput.fill("NEWBASE32SECRET");435    await popup.getByRole("button", { name: /save/i }).click();436    await popup.waitForTimeout(1500);437    await ctx.close();438  });439440  test("TOTP20 - delete TOTP from entry", async () => {441    const { popup } = await setupVault(ctx);442    await popup.locator('input[placeholder="search vault..."]').click();443    await popup.getByRole("button", { name: /new entry/i }).click();444    await popup.locator(".peach-new-name-input").fill("DeleteTOTP");445    await popup.locator('input[placeholder="domain.com"]').fill("https://deltotp.com");446    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");447    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");448    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");449    await popup.getByRole("button", { name: /create/i }).click();450    await popup.waitForTimeout(1500);451    await popup.locator(".peach-entry-name-button").first().click();452    await popup.waitForTimeout(500);453    await popup.locator("button[aria-label='Edit']").first().click();454    await popup.waitForTimeout(500);455    const totpInput = popup.locator("input[placeholder*='totp' i]");456    await totpInput.fill("");457    await popup.getByRole("button", { name: /save/i }).click();458    await popup.waitForTimeout(1500);459    await ctx.close();460  });461462  test("TOTP21 - multiple entries with TOTP", async () => {463    const { popup } = await setupVault(ctx);464    for (const name of ["TOTP1", "TOTP2", "TOTP3"]) {465      await popup.locator('input[placeholder="search vault..."]').click();466      await popup.getByRole("button", { name: /new entry/i }).click();467      await popup.locator(".peach-new-name-input").fill(name);468      await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);469      await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);470      await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");471      await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");472      await popup.getByRole("button", { name: /create/i }).click();473      await popup.waitForTimeout(800);474    }475    await expect(popup.locator("text=TOTP1")).toBeVisible({ timeout: 3000 });476    await ctx.close();477  });478479  test("TOTP22 - TOTP entry survives entry edit", async () => {480    const { popup } = await setupVault(ctx);481    await popup.locator('input[placeholder="search vault..."]').click();482    await popup.getByRole("button", { name: /new entry/i }).click();483    await popup.locator(".peach-new-name-input").fill("SurviveEdit");484    await popup.locator('input[placeholder="domain.com"]').fill("https://survive.com");485    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");486    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");487    await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");488    await popup.getByRole("button", { name: /create/i }).click();489    await popup.waitForTimeout(1500);490    await popup.locator(".peach-entry-name-button").first().click();491    await popup.waitForTimeout(500);492    await popup.locator("button[aria-label='Edit']").first().click();493    await popup.waitForTimeout(500);494    await popup.locator(".peach-new-name-input").fill("SurviveEditChanged");495    await popup.getByRole("button", { name: /save/i }).click();496    await popup.waitForTimeout(1500);497    await popup.locator(".peach-entry-name-button").first().click();498    await popup.waitForTimeout(500);499    const totpCode = popup.locator(".peach-totp-code");500    await expect(totpCode).toBeVisible({ timeout: 3000 });501    await ctx.close();502  });503});