bd082b4a23c5

playwright: point the specs at the list overlay — the top search bar is readOnly and only opens the list, so filling it or clicking new-entry from the vault stage could never work

time
2026-07-26 19:15 UTC · 3 months ago
machine
koh2
agent
claude-opus-5
session
playwright setupVault repair
parent
cce5d8227d4e
5 files tests/entry-crud.spec.ts+9 −2tests/helpers/vault.ts+17 −0tests/search.spec.ts+33 −30tests/vault-lifecycle.spec.ts+2 −2tests/vault-security.spec.ts+4 −2
tests/entry-crud.spec.ts+9 −2
@@ -1,5 +1,5 @@11 import { test, expect, type BrowserContext } from "@playwright/test";2-import { createEntryViaForm, makeContext, setupVault } from "./helpers/vault";2+import { createEntryViaForm, listSearch, makeContext, openList, setupVault } from "./helpers/vault";33 44 // Real vault creation (Argon2id + recovery confirmation ≈ 12s) plus a form55 // round-trip per test — the global 30s cap is too tight.@@ -18,6 +18,7 @@1818       username: "june",1919       password: "secret123!",2020     });21+    await openList(popup);2122     await expect(popup.locator(".peach-entry-row-name", { hasText: "GitHub" })).toBeVisible({ timeout: 5000 });2223   });2324 @@ -30,6 +31,7 @@3031       password: "secret123!",3132       totp: "JBSWY3DPEHPK3PXP",3233     });34+    await openList(popup);3335     await popup.locator(".peach-entry-row", { hasText: "GitHub2FA" }).click();3436     await expect(popup.locator(".peach-totp-code-text")).toHaveText(/\d{3} \d{3}/, { timeout: 5000 });3537   });@@ -37,10 +39,12 @@3739   test("EC03 - edited title persists", async () => {3840     const { popup } = await setupVault(ctx);3941     await createEntryViaForm(popup, { title: "EditTest", url: "https://edit.test", username: "u", password: "oldpass" });42+    await openList(popup);4043     await popup.locator(".peach-entry-row", { hasText: "EditTest" }).click();4144     await popup.locator(".peach-entry-name-button").click();4245     await popup.locator(".peach-inline-title-input").fill("EditedTitle");4346     await popup.keyboard.press("Enter");47+    await openList(popup);4448     await expect(popup.locator(".peach-entry-row-name", { hasText: "EditedTitle" })).toBeVisible({ timeout: 5000 });4549     await expect(popup.locator(".peach-entry-row-name", { hasText: "EditTest" })).toHaveCount(0);4650   });@@ -48,9 +52,11 @@4852   test("EC04 - delete entry removes it from the vault", async () => {4953     const { popup } = await setupVault(ctx);5054     await createEntryViaForm(popup, { title: "DeleteMe", url: "https://delete.test", username: "u", password: "p" });55+    await openList(popup);5156     await popup.locator(".peach-entry-row", { hasText: "DeleteMe" }).click();5257     await popup.locator("[aria-label='Move to trash']").click();5358     await popup.getByRole("button", { name: /^yes$/i }).click();59+    await openList(popup);5460     await expect(popup.locator(".peach-entry-row-name", { hasText: "DeleteMe" })).toHaveCount(0, { timeout: 5000 });5561   });5662 @@ -59,7 +65,8 @@5965     for (const name of ["Apple", "Amazon"]) {6066       await createEntryViaForm(popup, { title: name, url: `https://${name.toLowerCase()}.test`, username: "u", password: "p" });6167     }62-    await popup.locator('input[placeholder="search vault..."]').fill("Apple");68+    await openList(popup);69+    await listSearch(popup).fill("Apple");6370     await expect(popup.locator(".peach-entry-row-name", { hasText: "Apple" })).toBeVisible();6471     await expect(popup.locator(".peach-entry-row-name", { hasText: "Amazon" })).toHaveCount(0);6572   });

view file

tests/helpers/vault.ts+17 −0
@@ -154,6 +154,22 @@154154 }155155 156156 /**157+ * Open the entry list. The top search bar is readOnly — clicking it is what158+ * mounts the list overlay, which is where the real search input and the159+ * new-entry button live.160+ */161+export async function openList(popup: Page): Promise<void> {162+  if (await popup.locator(".peach-overlay-view .peach-search-input").isVisible().catch(() => false)) return;163+  await popup.locator(".peach-search-wrap .peach-search-input").click();164+  await expect(popup.locator(".peach-overlay-view .peach-search-input")).toBeVisible({ timeout: 5000 });165+}166+167+/** The editable search field, inside the list overlay. */168+export function listSearch(popup: Page) {169+  return popup.locator(".peach-overlay-view .peach-search-input");170+}171+172+/**157173  * Fill and submit the new-entry form. Selectors follow Add.tsx: the title field158174  * is .peach-new-title-input (it was .peach-new-name-input in an older UI, which159175  * is why the specs that still use that class fail to find it).@@ -162,6 +178,7 @@162178   popup: Page,163179   entry: { title: string; url?: string; username?: string; password?: string; totp?: string; notes?: string },164180 ): Promise<void> {181+  await openList(popup);165182   await popup.locator(".peach-new-entry-row").click();166183   await popup.locator(".peach-new-title-input").fill(entry.title);167184   if (entry.url) await popup.locator(".peach-domain-value--edit").fill(entry.url);

view file

tests/search.spec.ts+33 −30
@@ -1,6 +1,11 @@11 import { test, expect, Page, BrowserContext } from "@playwright/test";2-import { UNLOCKED, VAULT_PASSWORD, addEntry, makeContext, setupVault } from "./helpers/vault";2+import { UNLOCKED, VAULT_PASSWORD, addEntry, listSearch, makeContext, openList, setupVault } from "./helpers/vault";33 4+// The top bar's search field is readOnly — it only opens the list overlay,5+// where the editable field lives. Specs that filled the top field were6+// filling a readOnly input.7+const topSearch = (popup: Page) => popup.locator(".peach-search-wrap .peach-search-input");8+49 // Real vault creation runs Argon2id; seeded entries need a popup reload each.510 test.describe.configure({ timeout: 90_000 });611 @@ -20,6 +25,7 @@2025     await popup.getByRole("button", { name: /unlock/i }).click();2126   }2227   await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 20000 });28+  await openList(popup);2329 }2430 2531 test.describe("Search", () => {@@ -29,15 +35,13 @@2935 3036   test("S01 - search input is visible", async () => {3137     const { popup } = await setupVault(ctx);32-    const searchInput = popup.locator('input[placeholder="search vault..."]');33-    await expect(searchInput).toBeVisible();38+    await expect(topSearch(popup)).toBeVisible();3439     await ctx.close();3540   });3641 3742   test("S02 - search input has placeholder text", async () => {3843     const { popup } = await setupVault(ctx);39-    const searchInput = popup.locator('input[placeholder="search vault..."]');40-    const placeholder = await searchInput.getAttribute("placeholder");44+    const placeholder = await topSearch(popup).getAttribute("placeholder");4145     expect(placeholder).toContain("search");4246     await ctx.close();4347   });@@ -46,7 +50,7 @@4650     const { popup } = await setupVault(ctx);4751     await createEntry(popup, "GoogleEntry");4852     await createEntry(popup, "AmazonEntry");49-    await popup.locator('input[placeholder="search vault..."]').fill("Google");53+    await listSearch(popup).fill("Google");5054     await popup.waitForTimeout(300);5155     const entries = popup.locator(".peach-entry-row");5256     const count = await entries.count();@@ -57,7 +61,7 @@5761   test("S04 - search is case-insensitive", async () => {5862     const { popup } = await setupVault(ctx);5963     await createEntry(popup, "TestEntry");60-    await popup.locator('input[placeholder="search vault..."]').fill("test");64+    await listSearch(popup).fill("test");6165     await popup.waitForTimeout(300);6266     const entries = popup.locator(".peach-entry-row");6367     const count = await entries.count();@@ -68,7 +72,7 @@6872   test("S05 - search with no results shows empty state", async () => {6973     const { popup } = await setupVault(ctx);7074     await createEntry(popup, "Entry1");71-    await popup.locator('input[placeholder="search vault..."]').fill("nonexistent");75+    await listSearch(popup).fill("nonexistent");7276     await popup.waitForTimeout(500);7377     const noResults = popup.locator("text=/no results|not found|0 results/i");7478     const hasNoResults = await noResults.isVisible({ timeout: 2000 }).catch(() => false);@@ -80,9 +84,9 @@8084     const { popup } = await setupVault(ctx);8185     await createEntry(popup, "EntryOne");8286     await createEntry(popup, "EntryTwo");83-    await popup.locator('input[placeholder="search vault..."]').fill("EntryOne");87+    await listSearch(popup).fill("EntryOne");8488     await popup.waitForTimeout(300);85-    await popup.locator('input[placeholder="search vault..."]').clear();89+    await listSearch(popup).clear();8690     await popup.waitForTimeout(300);8791     const entries = popup.locator(".peach-entry-row");8892     const count = await entries.count();@@ -94,7 +98,7 @@9498     const { popup } = await setupVault(ctx);9599     await createEntry(popup, "Site1", "google.com");96100     await createEntry(popup, "Site2", "amazon.com");97-    await popup.locator('input[placeholder="search vault..."]').fill("google");101+    await listSearch(popup).fill("google");98102     await popup.waitForTimeout(300);99103     const entries = popup.locator(".peach-entry-row");100104     const count = await entries.count();@@ -106,7 +110,7 @@106110     const { popup } = await setupVault(ctx);107111     await createEntry(popup, "User1");108112     await createEntry(popup, "User2");109-    await popup.locator('input[placeholder="search vault..."]').fill("[email protected]");113+    await listSearch(popup).fill("[email protected]");110114     await popup.waitForTimeout(300);111115     const entries = popup.locator(".peach-entry-row");112116     const count = await entries.count();@@ -119,9 +123,9 @@119123     for (let i = 0; i < 15; i++) {120124       await createEntry(popup, `Entry${i}`, `site${i}.com`);121125     }122-    await popup.locator('input[placeholder="search vault..."]').fill("Entry5");126+    await listSearch(popup).fill("Entry5");123127     await popup.waitForTimeout(300);124-    await popup.locator('input[placeholder="search vault..."]').clear();128+    await listSearch(popup).clear();125129     await popup.waitForTimeout(300);126130     await ctx.close();127131   });@@ -129,7 +133,7 @@129133   test("S10 - search with special characters", async () => {130134     const { popup } = await setupVault(ctx);131135     await createEntry(popup, "Test@Entry");132-    await popup.locator('input[placeholder="search vault..."]').fill("@");136+    await listSearch(popup).fill("@");133137     await popup.waitForTimeout(300);134138     await ctx.close();135139   });@@ -137,15 +141,14 @@137141   test("S11 - search with regex characters", async () => {138142     const { popup } = await setupVault(ctx);139143     await createEntry(popup, "Test.Entry");140-    await popup.locator('input[placeholder="search vault..."]').fill(".");144+    await listSearch(popup).fill(".");141145     await popup.waitForTimeout(300);142146     await ctx.close();143147   });144148 145149   test("S12 - search input has proper aria-label", async () => {146150     const { popup } = await setupVault(ctx);147-    const searchInput = popup.locator('input[placeholder="search vault..."]');148-    const ariaLabel = await searchInput.getAttribute("aria-label");151+    const ariaLabel = await topSearch(popup).getAttribute("aria-label");149152     expect(ariaLabel).toBeTruthy();150153     await ctx.close();151154   });@@ -153,11 +156,11 @@153156   test("S13 - search clears on escape", async () => {154157     const { popup } = await setupVault(ctx);155158     await createEntry(popup, "TestEntry");156-    await popup.locator('input[placeholder="search vault..."]').fill("Test");159+    await listSearch(popup).fill("Test");157160     await popup.waitForTimeout(300);158161     await popup.keyboard.press("Escape");159162     await popup.waitForTimeout(200);160-    const value = await popup.locator('input[placeholder="search vault..."]').inputValue();163+    const value = await listSearch(popup).inputValue();161164     expect(value).toBe("");162165     await ctx.close();163166   });@@ -165,7 +168,7 @@165168   test("S14 - search debounce works", async () => {166169     const { popup } = await setupVault(ctx);167170     await createEntry(popup, "TestEntry");168-    const searchInput = popup.locator('input[placeholder="search vault..."]');171+    const searchInput = listSearch(popup);169172     await searchInput.fill("T");170173     await popup.waitForTimeout(100);171174     await searchInput.fill("Te");@@ -178,7 +181,7 @@178181   test("S15 - search highlights matching text", async () => {179182     const { popup } = await setupVault(ctx);180183     await createEntry(popup, "TestingEntry");181-    await popup.locator('input[placeholder="search vault..."]').fill("Test");184+    await listSearch(popup).fill("Test");182185     await popup.waitForTimeout(300);183186     const highlighted = popup.locator("[class*='highlight' i], [class*='match' i]").first();184187     const hasHighlight = await highlighted.isVisible().catch(() => false);@@ -190,7 +193,7 @@190193     const { popup } = await setupVault(ctx);191194     await createEntry(popup, "Entry1");192195     await createEntry(popup, "Entry2");193-    await popup.locator('input[placeholder="search vault..."]').fill("");196+    await listSearch(popup).fill("");194197     await popup.waitForTimeout(300);195198     const entries = popup.locator(".peach-entry-row");196199     const count = await entries.count();@@ -213,7 +216,7 @@213216   test("S18 - partial match works", async () => {214217     const { popup } = await setupVault(ctx);215218     await createEntry(popup, "MyPasswordEntry");216-    await popup.locator('input[placeholder="search vault..."]').fill("Pass");219+    await listSearch(popup).fill("Pass");217220     await popup.waitForTimeout(300);218221     const entries = popup.locator(".peach-entry-row");219222     const count = await entries.count();@@ -230,7 +233,7 @@230233   test("SA01 - search with spaces works", async () => {231234     const { popup } = await setupVault(ctx);232235     await createEntry(popup, "My Test Entry");233-    await popup.locator('input[placeholder="search vault..."]').fill("Test");236+    await listSearch(popup).fill("Test");234237     await popup.waitForTimeout(300);235238     const entries = popup.locator(".peach-entry-row");236239     const count = await entries.count();@@ -241,13 +244,13 @@241244   test("SA02 - search remembers previous query", async () => {242245     const { popup } = await setupVault(ctx);243246     await createEntry(popup, "Test1");244-    await popup.locator('input[placeholder="search vault..."]').fill("Test1");247+    await listSearch(popup).fill("Test1");245248     await popup.waitForTimeout(300);246249     await popup.locator(".peach-entry-name-button").first().click();247250     await popup.waitForTimeout(300);248251     await popup.keyboard.press("Escape");249252     await popup.waitForTimeout(200);250-    const value = await popup.locator('input[placeholder="search vault..."]').inputValue();253+    const value = await listSearch(popup).inputValue();251254     expect(value).toBe("Test1");252255     await ctx.close();253256   });@@ -255,7 +258,7 @@255258   test("SA03 - search field does not accept invalid characters", async () => {256259     const { popup } = await setupVault(ctx);257260     await createEntry(popup, "Test");258-    const searchInput = popup.locator('input[placeholder="search vault..."]');261+    const searchInput = listSearch(popup);259262     await searchInput.fill("Test\r\n");260263     await ctx.close();261264   });@@ -264,7 +267,7 @@264267     const { popup } = await setupVault(ctx);265268     await createEntry(popup, "Short");266269     const longQuery = "a".repeat(100);267-    await popup.locator('input[placeholder="search vault..."]').fill(longQuery);270+    await listSearch(popup).fill(longQuery);268271     await popup.waitForTimeout(300);269272     await ctx.close();270273   });@@ -272,7 +275,7 @@272275   test("SA05 - search in non-latin characters", async () => {273276     const { popup } = await setupVault(ctx);274277     await createEntry(popup, "Тест");275-    await popup.locator('input[placeholder="search vault..."]').fill("Тест");278+    await listSearch(popup).fill("Тест");276279     await popup.waitForTimeout(300);277280     await ctx.close();278281   });

view file

tests/vault-lifecycle.spec.ts+2 −2
@@ -112,7 +112,7 @@112112     await addEntry(popup, { title: "LockRoundTrip", url: "https://roundtrip.test", username: "[email protected]", password: "entry-secret" });113113     await lockVault(popup);114114     await unlockVault(popup);115-    await expect(popup.getByText("LockRoundTrip")).toBeVisible({ timeout: 10000 });115+    await expect(popup.locator(".peach-entry-name-text").first()).toHaveText("LockRoundTrip", { timeout: 10000 });116116   });117117 118118   test("VL10 - reopening the popup requires the password again", async () => {@@ -124,7 +124,7 @@124124     await reopened.goto(`chrome-extension://${extId}/src/popup/index.html`);125125     await expect(reopened.locator("#unlock-password")).toBeVisible({ timeout: 15000 });126126     await unlockVault(reopened);127-    await expect(reopened.getByText("ReopenTest")).toBeVisible({ timeout: 10000 });127+    await expect(reopened.locator(".peach-entry-name-text").first()).toHaveText("ReopenTest", { timeout: 10000 });128128   });129129 130130   test("VL11 - a wrong password does not unlock a reopened popup", async () => {

view file

tests/vault-security.spec.ts+4 −2
@@ -9,6 +9,8 @@99   createEntryViaForm,1010   lockVault,1111   makeContext,12+  listSearch,13+  openList,1214   openSettings,1315   setupVault,1416   waitForSW,@@ -73,8 +75,8 @@7375   test("SECX05 - search input sanitizes query before display", async () => {7476     const { popup } = await setupVault(ctx);7577     await createEntryViaForm(popup, { title: "SearchTest", url: "https://search.com", username: "[email protected]", password: "pass" });76-    const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();77-    await searchInput.click();78+    await openList(popup);79+    const searchInput = listSearch(popup);7880     await searchInput.fill('<script>alert("xss")</script>');7981     await popup.waitForTimeout(300);8082     const value = await searchInput.inputValue();

view file