90d2d69e8d21
test: Retire false-green browser suites (v4.10.38)
- time
- 2026-08-29 13:14 UTC · 1 month ago
- machine
- koh2
- parent
- 29e0ff7cbd5d
↓ peach-browser-90d2d69e.tar.gzatlas at this save
files changed23 files
bun.lock+3 −6manifest.firefox.json+1 −1manifest.json+1 −1package.json+2 −2tests/a11y-totp-vault.spec.ts+0 −412tests/autofill-deep.spec.ts+0 −631tests/autofill.spec.ts+0 −156tests/clipboard.spec.ts+0 −377tests/codex-pdf-restore.spec.ts+1 −2tests/copy-totp.spec.ts+0 −156tests/entry-comprehensive.spec.ts+0 −682tests/entry-flows-state.spec.ts+0 −396tests/helpers/vault.ts+2 −2tests/import-export-robust.spec.ts+0 −685tests/keyboard-comprehensive.spec.ts+0 −787tests/keyboard-edge.spec.ts+0 −512tests/passkeys.spec.ts+1 −2tests/password-generator.spec.ts+0 −464tests/security-visual.spec.ts+0 −338tests/settings-persistence.spec.ts+0 −637tests/totp-edge.spec.ts+0 −503tests/vault-security.spec.ts+2 −3tests/visual-compliance.spec.ts+0 −624bun.lock+3 −6
@@ -36,7 +36,6 @@3636 "@types/papaparse": "^5.5.2",3737 "chrome-webstore-upload-cli": "^4.0.0",3838 "jsdom": "^29.0.2",39- "playwright": "^1.59.1",4039 "typescript": "^6.0.3",4140 "vite": "^8.0.8",4241 "vite-plugin-solid": "^2.11.12",@@ -661,7 +660,7 @@661660 662661 "fs-extra": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],663662 664- "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],663+ "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],665664 666665 "fx-runner": ["[email protected]", "", { "dependencies": { "commander": "2.9.0", "shell-quote": "1.7.3", "spawn-sync": "1.0.15", "when": "3.7.7", "which": "1.2.4", "winreg": "0.0.12" }, "bin": "bin/fx-runner" }, "sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg=="],667666 @@ -1273,19 +1272,17 @@12731272 12741273 "pdf-lib/pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],12751274 1275+ "playwright/fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],1276+12761277 "rc/ini": ["[email protected]", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],12771278 12781279 "rc/strip-json-comments": ["[email protected]", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],12791280 1280- "rollup/fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],1281-12821281 "rxjs/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],12831282 12841283 "thread-stream/real-require": ["[email protected]", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],12851284 12861285 "update-notifier/semver": ["[email protected]", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],1287-1288- "vite/fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],12891286 12901287 "vitest/es-module-lexer": ["[email protected]", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],12911288 manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.10.37",4+ "version": "4.10.38",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "4.10.37",5+ "version": "4.10.38",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+2 −2
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.10.37",3+ "version": "4.10.38",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",@@ -18,6 +18,7 @@1818 "typecheck": "tsc --noEmit",1919 "test": "vitest run",2020 "test:watch": "vitest",21+ "test:e2e": "npm run build:chrome && playwright test tests/vault-lifecycle.spec.ts tests/entry-crud.spec.ts tests/search.spec.ts tests/settings.spec.ts tests/lock-screen.spec.ts tests/vault-security.spec.ts tests/sync-alerts.spec.ts",2122 "test:passkeys": "npm run build:e2e && playwright test tests/passkeys.spec.ts",2223 "ci": "bun run scripts/ci.ts",2324 "package:source": "zip -r peach-source.zip src scripts public manifest.json manifest.firefox.json package.json bun.lock tsconfig.json vite.config.ts --exclude 'node_modules/*' --exclude 'dist-*/*' --exclude '*.zip'",@@ -63,7 +64,6 @@6364 "@types/papaparse": "^5.5.2",6465 "chrome-webstore-upload-cli": "^4.0.0",6566 "jsdom": "^29.0.2",66- "playwright": "^1.59.1",6767 "typescript": "^6.0.3",6868 "vite": "^8.0.8",6969 "vite-plugin-solid": "^2.11.12",tests/a11y-totp-vault.spec.ts+0 −412
@@ -1,412 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async function makeContext() {12- const { chromium } = await import("playwright");13- const userDataDir = join(os.tmpdir(), `peach-a11y-${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-}20-21-async 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-}31-32-async 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-}46-47-test.describe("Accessibility", () => {48-49- test("A11Y01 - all buttons have accessible names", async () => {50- const ctx = await makeContext();51- const { popup } = await setupVault(ctx);52- const buttons = popup.locator("button");53- const count = await buttons.count();54- let allAccessible = true;55- for (let i = 0; i < Math.min(count, 20); i++) {56- const btn = buttons.nth(i);57- const ariaLabel = await btn.getAttribute("aria-label");58- const text = await btn.textContent().catch(() => "");59- if (!ariaLabel && !text?.trim()) {60- allAccessible = false;61- break;62- }63- }64- expect(allAccessible).toBe(true);65- await ctx.close();66- });67-68- test("A11Y02 - form inputs have associated labels", async () => {69- const ctx = await makeContext();70- const { popup } = await setupVault(ctx);71- const inputs = popup.locator("input:not([type='hidden']):not([type='checkbox'])");72- const count = await inputs.count();73- let allLabeled = true;74- for (let i = 0; i < Math.min(count, 10); i++) {75- const input = inputs.nth(i);76- const id = await input.getAttribute("id");77- const ariaLabel = await input.getAttribute("aria-label");78- const ariaLabelledby = await input.getAttribute("aria-labelledby");79- const placeholder = await input.getAttribute("placeholder");80- if (!id && !ariaLabel && !ariaLabelledby && !placeholder) {81- allLabeled = false;82- break;83- }84- }85- expect(allLabeled).toBe(true);86- await ctx.close();87- });88-89- test("A11Y03 - color contrast for text elements", async () => {90- const ctx = await makeContext();91- const { popup } = await setupVault(ctx);92- const textColor = await popup.evaluate(() => {93- const el = document.querySelector(".peach-entry-name, .peach-label, body");94- return window.getComputedStyle(el!).color;95- });96- const bgColor = await popup.evaluate(() => {97- return window.getComputedStyle(document.body).backgroundColor;98- });99- expect(textColor).toBeTruthy();100- expect(bgColor).toBeTruthy();101- await ctx.close();102- });103-104- test("A11Y04 - focus indicators visible on interactive elements", async () => {105- const ctx = await makeContext();106- const { popup } = await setupVault(ctx);107- await popup.locator('input[placeholder="search vault..."]').click();108- await popup.keyboard.press("Tab");109- const focusedEl = await popup.evaluate(() => document.activeElement?.tagName);110- expect(focusedEl).toBeTruthy();111- await ctx.close();112- });113-114- test("A11Y05 - ARIA roles used correctly", async () => {115- const ctx = await makeContext();116- const { popup } = await setupVault(ctx);117- const switchRole = popup.locator("[role='switch']");118- const optionRole = popup.locator("[role='option']");119- const hasSwitch = await switchRole.count();120- const hasOption = await optionRole.count();121- expect(hasSwitch + hasOption).toBeGreaterThan(0);122- await ctx.close();123- });124-125- test("A11Y06 - screen reader announces copy feedback", async () => {126- const ctx = await makeContext();127- const { popup } = await setupVault(ctx);128- await popup.locator('input[placeholder="search vault..."]').click();129- await popup.getByRole("button", { name: /new entry/i }).click();130- await popup.locator(".peach-new-name-input").fill("A11YTest");131- await popup.locator('input[placeholder="domain.com"]').fill("https://a11y.com");132- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");133- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");134- await pwdField.fill("pass123");135- await popup.getByRole("button", { name: /create/i }).click();136- await popup.waitForTimeout(1500);137- const copyBtn = popup.locator(".peach-copy-btn").first();138- if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {139- await copyBtn.click();140- await popup.waitForTimeout(300);141- }142- await ctx.close();143- });144-145- test("A11Y07 - landmark regions identified", async () => {146- const ctx = await makeContext();147- const { popup } = await setupVault(ctx);148- const mainLandmark = popup.locator("main, [role='main']");149- const hasMain = await mainLandmark.count();150- expect(hasMain).toBeGreaterThan(0);151- await ctx.close();152- });153-154- test("A11Y08 - lang attribute on html element", async () => {155- const ctx = await makeContext();156- const extId = await waitForSW(ctx);157- const popup = await ctx.newPage();158- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);159- await popup.waitForTimeout(2000);160- const lang = await popup.getAttribute("html", "lang");161- expect(lang).toBeTruthy();162- await ctx.close();163- });164-});165-166-test.describe("TOTP Functionality", () => {167-168- test("TOTP01 - TOTP field accepts base32 secret", async () => {169- const ctx = await makeContext();170- const { popup } = await setupVault(ctx);171- await popup.locator('input[placeholder="search vault..."]').click();172- await popup.getByRole("button", { name: /new entry/i }).click();173- await popup.locator(".peach-new-name-input").fill("TOTPBase32");174- await popup.locator('input[placeholder="domain.com"]').fill("https://totp.com");175- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");176- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");177- await pwdField.fill("pass123");178- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");179- await popup.getByRole("button", { name: /create/i }).click();180- await popup.waitForTimeout(1500);181- await ctx.close();182- });183-184- test("TOTP02 - TOTP code displays in entry view", async () => {185- const ctx = await makeContext();186- const { popup } = await setupVault(ctx);187- await popup.locator('input[placeholder="search vault..."]').click();188- await popup.getByRole("button", { name: /new entry/i }).click();189- await popup.locator(".peach-new-name-input").fill("TOTPDisplay");190- await popup.locator('input[placeholder="domain.com"]').fill("https://display.com");191- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");192- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");193- await pwdField.fill("pass123");194- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");195- await popup.getByRole("button", { name: /create/i }).click();196- await popup.waitForTimeout(2000);197- await popup.locator(".peach-entry-name-button").first().click();198- await popup.waitForTimeout(1000);199- const totpCode = popup.locator(".peach-totp-code");200- await expect(totpCode).toBeVisible({ timeout: 5000 });201- await ctx.close();202- });203-204- test("TOTP03 - TOTP countdown timer shows remaining seconds", async () => {205- const ctx = await makeContext();206- const { popup } = await setupVault(ctx);207- await popup.locator('input[placeholder="search vault..."]').click();208- await popup.getByRole("button", { name: /new entry/i }).click();209- await popup.locator(".peach-new-name-input").fill("TOTPTimer");210- await popup.locator('input[placeholder="domain.com"]').fill("https://timer.com");211- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");212- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");213- await pwdField.fill("pass123");214- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");215- await popup.getByRole("button", { name: /create/i }).click();216- await popup.waitForTimeout(1500);217- await popup.locator("text=TOTPTimer").first().click();218- await popup.waitForTimeout(500);219- const countdown = popup.locator(".peach-totp-countdown");220- await expect(countdown).toBeVisible({ timeout: 3000 });221- const remaining = await countdown.textContent();222- expect(parseInt(remaining ?? "0")).toBeGreaterThan(0);223- await ctx.close();224- });225-226- test("TOTP04 - copy TOTP button works", async () => {227- const ctx = await makeContext();228- const { popup } = await setupVault(ctx);229- await popup.locator('input[placeholder="search vault..."]').click();230- await popup.getByRole("button", { name: /new entry/i }).click();231- await popup.locator(".peach-new-name-input").fill("TOTPCopy");232- await popup.locator('input[placeholder="domain.com"]').fill("https://copytotp.com");233- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");234- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");235- await pwdField.fill("pass123");236- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");237- await popup.getByRole("button", { name: /create/i }).click();238- await popup.waitForTimeout(1500);239- await popup.locator("text=TOTPCopy").first().click();240- await popup.waitForTimeout(500);241- const totpCopyBtn = popup.locator("[aria-label='Copy TOTP code']");242- if (await totpCopyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {243- await totpCopyBtn.click();244- await popup.waitForTimeout(300);245- }246- await ctx.close();247- });248-249- test("TOTP05 - TOTP ring progress animates", async () => {250- const ctx = await makeContext();251- const { popup } = await setupVault(ctx);252- await popup.locator('input[placeholder="search vault..."]').click();253- await popup.getByRole("button", { name: /new entry/i }).click();254- await popup.locator(".peach-new-name-input").fill("TOTPRing");255- await popup.locator('input[placeholder="domain.com"]').fill("https://ring.com");256- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");257- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");258- await pwdField.fill("pass123");259- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");260- await popup.getByRole("button", { name: /create/i }).click();261- await popup.waitForTimeout(1500);262- await popup.locator("text=TOTPRing").first().click();263- await popup.waitForTimeout(500);264- const ring = popup.locator(".peach-totp-ring");265- await expect(ring).toBeVisible({ timeout: 3000 });266- await ctx.close();267- });268-269- test("TOTP06 - entry without TOTP shows no totp UI", async () => {270- const ctx = await makeContext();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("NoTOTP");275- await popup.locator('input[placeholder="domain.com"]').fill("https://nototp.com");276- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");277- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");278- await pwdField.fill("pass123");279- await popup.getByRole("button", { name: /create/i }).click();280- await popup.waitForTimeout(1500);281- await popup.locator("text=NoTOTP").first().click();282- await popup.waitForTimeout(500);283- const totpBand = popup.locator(".peach-totp-band");284- const isHidden = await totpBand.isHidden().catch(() => true);285- expect(isHidden).toBe(true);286- await ctx.close();287- });288-});289-290-test.describe("Vault Management", () => {291-292- test("VAULT01 - create vault with different passwords shows mismatch error", async () => {293- const ctx = await makeContext();294- const extId = await waitForSW(ctx);295- const popup = await ctx.newPage();296- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);297- await popup.waitForTimeout(2000);298- await popup.locator("#master-password").fill("password123");299- await popup.locator("#confirm-password").fill("differentpassword");300- await popup.getByRole("button", { name: /create vault/i }).click();301- await popup.waitForTimeout(500);302- const error = popup.locator(".peach-hint-error");303- await expect(error).toBeVisible({ timeout: 3000 });304- await ctx.close();305- });306-307- test("VAULT02 - clear vault option exists in settings", async () => {308- const ctx = await makeContext();309- const { popup } = await setupVault(ctx);310- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();311- await popup.waitForTimeout(300);312- await ctx.close();313- });314-315- test("VAULT04 - vault persists through navigation", async () => {316- const ctx = await makeContext();317- const { popup } = await setupVault(ctx);318- await popup.locator('input[placeholder="search vault..."]').click();319- await popup.getByRole("button", { name: /new entry/i }).click();320- await popup.locator(".peach-new-name-input").fill("PersistNav");321- await popup.locator('input[placeholder="domain.com"]').fill("https://persistnav.com");322- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");323- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");324- await pwdField.fill("pass123");325- await popup.getByRole("button", { name: /create/i }).click();326- await popup.waitForTimeout(1500);327- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();328- await popup.waitForTimeout(300);329- await popup.getByRole("button", { name: /password generator/i }).click();330- await popup.waitForTimeout(500);331- await popup.keyboard.press("Escape");332- await popup.waitForTimeout(300);333- const entry = popup.locator("text=PersistNav");334- await expect(entry).toBeVisible({ timeout: 3000 });335- await ctx.close();336- });337-338- test("VAULT05 - re-lock and unlock vault", async () => {339- const ctx = await makeContext();340- const { popup } = await setupVault(ctx);341- const lockBtn = popup.locator(".peach-lock-btn, [aria-label='Lock vault']").first();342- if (await lockBtn.isVisible({ timeout: 2000 }).catch(() => false)) {343- await lockBtn.click();344- await popup.waitForTimeout(1000);345- await popup.locator("#master-password").fill("password123");346- await popup.getByRole("button", { name: /unlock/i }).click();347- await popup.waitForTimeout(2000);348- const entries = popup.locator("[class*=entry]");349- await expect(entries.first()).toBeVisible({ timeout: 5000 });350- }351- await ctx.close();352- });353-354- test("VAULT06 - search works with Cyrillic text", async () => {355- const ctx = await makeContext();356- const { popup } = await setupVault(ctx);357- await popup.locator('input[placeholder="search vault..."]').click();358- await popup.getByRole("button", { name: /new entry/i }).click();359- await popup.locator(".peach-new-name-input").fill("ТестПароль");360- await popup.locator('input[placeholder="domain.com"]').fill("https://тест.com");361- await popup.locator('input[placeholder="username or email"]').fill("пользователь@тест.com");362- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");363- await pwdField.fill("пароль123");364- await popup.getByRole("button", { name: /create/i }).click();365- await popup.waitForTimeout(1500);366- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();367- await searchInput.click();368- await searchInput.fill("Тест");369- await popup.waitForTimeout(300);370- await expect(popup.locator("text=ТестПароль")).toBeVisible({ timeout: 3000 });371- await ctx.close();372- });373-374- test("VAULT07 - entries can be sorted", async () => {375- const ctx = await makeContext();376- const { popup } = await setupVault(ctx);377- for (const name of ["Zebra", "Apple", "Mango"]) {378- await popup.locator('input[placeholder="search vault..."]').click();379- await popup.getByRole("button", { name: /new entry/i }).click();380- await popup.locator(".peach-new-name-input").fill(name);381- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);382- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);383- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");384- await pwdField.fill("pass123");385- await popup.getByRole("button", { name: /create/i }).click();386- await popup.waitForTimeout(500);387- }388- await ctx.close();389- });390-391- test("VAULT08 - delete entry removes it from list", async () => {392- const ctx = await makeContext();393- const { popup } = await setupVault(ctx);394- await popup.locator('input[placeholder="search vault..."]').click();395- await popup.getByRole("button", { name: /new entry/i }).click();396- await popup.locator(".peach-new-name-input").fill("DeleteMe");397- await popup.locator('input[placeholder="domain.com"]').fill("https://deleteme.com");398- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");399- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");400- await pwdField.fill("pass123");401- await popup.getByRole("button", { name: /create/i }).click();402- await popup.waitForTimeout(1500);403- await popup.locator("text=DeleteMe").first().click();404- await popup.waitForTimeout(500);405- const deleteLink = popup.locator("text=delete entry");406- if (await deleteLink.isVisible({ timeout: 2000 }).catch(() => false)) {407- await deleteLink.click();408- await popup.waitForTimeout(1000);409- }410- await ctx.close();411- });412-});\ No newline at end of filetests/autofill-deep.spec.ts+0 −631
@@ -1,631 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async 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-}20-21-async 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-}31-32-async 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-}46-47-test.describe("Content Script Injection", () => {48- let ctx: BrowserContext;49- test.afterEach(async () => { await ctx.close(); });50- test.beforeEach(async () => { ctx = await makeContext(); });51-52- 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- });66-67- 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- });77-78- 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- });95-96- 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- });114-115- 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- });131-132- 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-});149-150-test.describe("Autofill Trigger Behavior", () => {151- let ctx: BrowserContext;152- test.afterEach(async () => { await ctx.close(); });153- test.beforeEach(async () => { ctx = await makeContext(); });154-155- 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- });182-183- 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- });201-202- 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- });224-225- 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- });252-253- 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- });275-276- 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-});296-297-test.describe("Form Detection Edge Cases", () => {298- let ctx: BrowserContext;299- test.afterEach(async () => { await ctx.close(); });300- test.beforeEach(async () => { ctx = await makeContext(); });301-302- 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- });313-314- 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- });328-329- 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- });336-337- 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- });351-352- 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- });359-360- 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- });374-375- 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- });385-386- 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- });401-402- 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-});419-420-test.describe("Credential Matching Logic", () => {421- let ctx: BrowserContext;422- test.afterEach(async () => { await ctx.close(); });423- test.beforeEach(async () => { ctx = await makeContext(); });424-425- 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- });447-448- 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- });470-471- 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- });495-496- 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- });511-512- 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-});528-529-test.describe("Autofill Security", () => {530- let ctx: BrowserContext;531- test.afterEach(async () => { await ctx.close(); });532- test.beforeEach(async () => { ctx = await makeContext(); });533-534- 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- });555-556- 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- });571-572- 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- });595-596- 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- });609-610- 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- });620-621- 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-});\ No newline at end of filetests/autofill.spec.ts+0 −156
@@ -1,156 +0,0 @@1-import { test, expect } from "@playwright/test";2-import type { BrowserContext } from "@playwright/test";3-import { fileURLToPath } from "url";4-import { dirname, join } from "path";5-import os from "os";6-import fs from "fs/promises";7-8-const __filename = fileURLToPath(import.meta.url);9-const __dirname = dirname(__filename);10-const EXTENSION_PATH = join(__dirname, "..", "dist");11-12-async function makeContext() {13- const { chromium } = await import("playwright");14- const userDataDir = join(os.tmpdir(), `peach-test-${Date.now()}`);15- await fs.mkdir(userDataDir, { recursive: true });16- return chromium.launchPersistentContext(userDataDir, {17- headless: false,18- args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],19- });20-}21-22-async function setupVault(ctx: BrowserContext): Promise<{ popup: import("@playwright/test").Page, extId: string }> {23- const maxWait = 15000;24- const start = Date.now();25- let sw = ctx.serviceWorkers()[0];26- while (!sw && Date.now() - start < maxWait) {27- await new Promise(r => setTimeout(r, 500));28- sw = ctx.serviceWorkers()[0];29- }30- if (!sw) throw new Error(`No SW after ${maxWait}ms`);31- const extId = new URL(sw.url()).host;32- const popup = await ctx.newPage();33- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);34- await popup.waitForTimeout(2000);35- const pw = popup.locator("#master-password");36- if (await pw.isVisible()) {37- await pw.fill("password123");38- await popup.locator("#confirm-password").fill("password123");39- await popup.getByRole("button", { name: /create vault/i }).click();40- await popup.waitForTimeout(2000);41- }42- return { popup, extId };43-}44-45-test.describe("Autofill Content Script", () => {46- let ctx: BrowserContext;47- test.afterEach(async () => { await ctx.close(); });48- test.beforeEach(async () => { ctx = await makeContext(); });49-50- test("31 - peach icon injected into page with password field", async () => {51- const { popup: _popup } = await setupVault(ctx);52- const testPage = await ctx.newPage();53- await testPage.setContent(`54- <html><body>55- <form action="/login" method="post">56- <input type="text" id="username" name="username" autocomplete="username">57- <input type="password" id="password" name="password" autocomplete="current-password">58- <button type="submit">Login</button>59- </form>60- </body></html>61- `);62- await testPage.waitForTimeout(2000);63- testPage.locator("button", { hasText: "🍑" });64- await testPage.waitForTimeout(500);65- });66-67- test("32 - autofill selector appears on icon click", 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("TestForm");72- await popup.locator('input[placeholder="domain.com"]').fill("https://example.com");73- await popup.locator('input[placeholder="username or email"]').fill("testuser");74- await popup.locator('input[placeholder="password"]').fill("testpass");75- await popup.getByRole("button", { name: /create/i }).click();76- await popup.waitForTimeout(1500);77- const testPage = await ctx.newPage();78- await testPage.setContent(`79- <html><body>80- <form>81- <input type="text" id="u" autocomplete="username">82- <input type="password" id="p" autocomplete="current-password">83- </form>84- </body></html>85- `);86- await testPage.waitForTimeout(2000);87- await testPage.waitForTimeout(500);88- });89-90- test("33 - no fill without user interaction", async () => {91- const { popup } = await setupVault(ctx);92- await popup.locator('input[placeholder="search vault..."]').click();93- await popup.getByRole("button", { name: /new entry/i }).click();94- await popup.locator(".peach-new-name-input").fill("SilentFillTest");95- await popup.locator('input[placeholder="domain.com"]').fill("https://silentfill.com");96- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");97- await popup.locator('input[placeholder="password"]').fill("silentpass");98- await popup.getByRole("button", { name: /create/i }).click();99- await popup.waitForTimeout(1500);100- const testPage = await ctx.newPage();101- await testPage.setContent(`<html><body><input type="password" id="p"></body></html>`);102- await testPage.waitForTimeout(1000);103- const pwInput = testPage.locator("#p");104- const value = await pwInput.inputValue();105- expect(value).toBe("");106- });107-108- test("34 - hidden password fields are not filled", async () => {109- const testPage = await ctx.newPage();110- await testPage.setContent(`111- <html><body>112- <input type="password" id="p" style="display:none">113- </body></html>114- `);115- await testPage.waitForTimeout(1000);116- const pwInput = testPage.locator("#p");117- const value = await pwInput.inputValue();118- expect(value).toBe("");119- });120-});121-122-test.describe("Import Export", () => {123- let ctx: BrowserContext;124- test.afterEach(async () => { await ctx.close(); });125- test.beforeEach(async () => { ctx = await makeContext(); });126-127- test("35 - export produces downloadable JSON file", async () => {128- const { popup } = await setupVault(ctx);129- await popup.locator('input[placeholder="search vault..."]').click();130- await popup.getByRole("button", { name: /new entry/i }).click();131- await popup.locator(".peach-new-name-input").fill("ExportTest");132- await popup.locator('input[placeholder="domain.com"]').fill("https://exporttest.com");133- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");134- await popup.locator(".peach-new-field-input[placeholder='password']").fill("exportpass");135- await popup.getByRole("button", { name: /create/i }).click();136- await popup.waitForTimeout(1500);137- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();138- await popup.waitForTimeout(300);139- await popup.getByRole("button", { name: /export backup/i }).click();140- await popup.waitForTimeout(500);141- const exportPasswordInput = popup.locator(".form-input[type='password']");142- await exportPasswordInput.fill("exportpassword");143- const downloadBtn = popup.getByRole("button", { name: /download backup/i });144- await expect(downloadBtn).toBeVisible();145- });146-147- test("36 - CSV import requires column mapping", async () => {148- const { popup } = await setupVault(ctx);149- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();150- await popup.waitForTimeout(300);151- await popup.getByRole("button", { name: /import passwords/i }).click();152- await popup.waitForTimeout(500);153- const fileInput = popup.locator('input[type="file"]');154- await expect(fileInput).toBeAttached();155- });156-});\ No newline at end of filetests/clipboard.spec.ts+0 −377
@@ -1,377 +0,0 @@1-import { test, expect, Page, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {12- const start = Date.now();13- let sw = ctx.serviceWorkers()[0];14- while (!sw && Date.now() - start < maxWait) {15- await new Promise(r => setTimeout(r, 500));16- sw = ctx.serviceWorkers()[0];17- }18- if (!sw) throw new Error(`No SW after ${maxWait}ms`);19- return new URL(sw.url()).host;20-}21-22-async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> {23- const extId = await waitForSW(ctx);24- const popup = await ctx.newPage();25- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);26- await popup.waitForTimeout(3000);27- return { popup, extId };28-}29-30-async function makeContext() {31- const { chromium } = await import("playwright");32- const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`);33- await fs.mkdir(userDataDir, { recursive: true });34- return chromium.launchPersistentContext(userDataDir, {35- headless: false,36- args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],37- });38-}39-40-async function setupVault(ctx: BrowserContext, password = "password123") {41- const { popup } = await loadPopup(ctx);42- const pw = popup.locator("#master-password");43- if (await pw.isVisible()) {44- await pw.fill(password);45- await popup.locator("#confirm-password").fill(password);46- await popup.getByRole("button", { name: /create vault/i }).click();47- await popup.waitForTimeout(2000);48- }49- return { popup };50-}51-52-async function createEntry(popup: Page, name: string) {53- await popup.locator('input[placeholder="search vault..."]').click();54- await popup.getByRole("button", { name: /new entry/i }).click();55- await popup.waitForTimeout(300);56- await popup.locator(".peach-new-name-input").fill(name);57- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);58- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);59- await popup.locator(".peach-new-field-input[placeholder='password']").fill("testpass123");60- await popup.getByRole("button", { name: /create/i }).click();61- await popup.waitForTimeout(800);62-}63-64-test.describe("Clipboard - Copy Password", () => {65- let ctx: BrowserContext;66- test.afterEach(async () => { await ctx.close(); });67- test.beforeEach(async () => { ctx = await makeContext(); });68-69- test("CP01 - copy button is visible in entry detail", async () => {70- const { popup } = await setupVault(ctx);71- await createEntry(popup, "CopyTest");72- await popup.locator(".peach-entry-name-button").first().click();73- await popup.waitForTimeout(300);74- const copyBtn = popup.locator("[aria-label*='copy' i]").first();75- await expect(copyBtn).toBeVisible();76- await ctx.close();77- });78-79- test("CP02 - copy button has correct aria-label", async () => {80- const { popup } = await setupVault(ctx);81- await createEntry(popup, "CopyTest");82- await popup.locator(".peach-entry-name-button").first().click();83- await popup.waitForTimeout(300);84- const copyBtn = popup.locator("[aria-label*='copy' i]").first();85- const ariaLabel = await copyBtn.getAttribute("aria-label");86- expect(ariaLabel).toContain("copy");87- await ctx.close();88- });89-90- test("CP03 - copy button copies password to clipboard", async () => {91- const { popup } = await setupVault(ctx);92- await createEntry(popup, "CopyTest");93- await popup.locator(".peach-entry-name-button").first().click();94- await popup.waitForTimeout(300);95- const copyBtn = popup.locator("[aria-label*='copy' i]").first();96- await copyBtn.click();97- await popup.waitForTimeout(300);98- const copied = popup.locator("text=/copied|\\u2713/i");99- const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false);100- expect(hasCopied).toBeTruthy();101- await ctx.close();102- });103-104- test("CP04 - copy shows temporary notification", async () => {105- const { popup } = await setupVault(ctx);106- await createEntry(popup, "CopyTest");107- await popup.locator(".peach-entry-name-button").first().click();108- await popup.waitForTimeout(300);109- const copyBtn = popup.locator("[aria-label*='copy' i]").first();110- await copyBtn.click();111- await popup.waitForTimeout(500);112- const notification = popup.locator("text=/copied to clipboard|✓ copied/i");113- const hasNotification = await notification.isVisible().catch(() => false);114- expect(hasNotification).toBeTruthy();115- await ctx.close();116- });117-118- test("CP05 - copied notification disappears after timeout", async () => {119- const { popup } = await setupVault(ctx);120- await createEntry(popup, "CopyTest");121- await popup.locator(".peach-entry-name-button").first().click();122- await popup.waitForTimeout(300);123- const copyBtn = popup.locator("[aria-label*='copy' i]").first();124- await copyBtn.click();125- await popup.waitForTimeout(3000);126- const notification = popup.locator("text=/copied/i");127- const isGone = await notification.isHidden().catch(() => false);128- expect(isGone).toBeTruthy();129- await ctx.close();130- });131-132- test("CP06 - copy password works with keyboard shortcut", async () => {133- const { popup } = await setupVault(ctx);134- await createEntry(popup, "CopyTest");135- await popup.locator(".peach-entry-name-button").first().click();136- await popup.waitForTimeout(300);137- await popup.keyboard.press("Control+Shift+C");138- await popup.waitForTimeout(500);139- await ctx.close();140- });141-142- test("CP07 - copy button is disabled during loading", async () => {143- const { popup } = await setupVault(ctx);144- await createEntry(popup, "CopyTest");145- await popup.locator(".peach-entry-name-button").first().click();146- await popup.waitForTimeout(300);147- const copyBtn = popup.locator("[aria-label*='copy' i]").first();148- await copyBtn.click();149- await popup.waitForTimeout(200);150- await ctx.close();151- });152-153- test("CP08 - copy clears clipboard after timeout", async () => {154- const { popup } = await setupVault(ctx);155- await createEntry(popup, "CopyTest");156- await popup.locator(".peach-entry-name-button").first().click();157- await popup.waitForTimeout(300);158- const copyBtn = popup.locator("[aria-label*='copy' i]").first();159- await copyBtn.click();160- await popup.waitForTimeout(45000);161- await ctx.close();162- });163-164- test("CP09 - multiple copy operations work", async () => {165- const { popup } = await setupVault(ctx);166- await createEntry(popup, "Entry1");167- await createEntry(popup, "Entry2");168- await popup.locator(".peach-entry-name-button").first().click();169- await popup.waitForTimeout(300);170- const copyBtn = popup.locator("[aria-label*='copy' i]").first();171- await copyBtn.click();172- await popup.waitForTimeout(500);173- await popup.keyboard.press("Escape");174- await popup.waitForTimeout(200);175- await popup.locator(".peach-entry-name-button").first().click();176- await popup.waitForTimeout(300);177- const newCopyBtn = popup.locator("[aria-label*='copy' i]").first();178- await newCopyBtn.click();179- await popup.waitForTimeout(300);180- await ctx.close();181- });182-183- test("CP10 - copy button has tooltip", async () => {184- const { popup } = await setupVault(ctx);185- await createEntry(popup, "CopyTest");186- await popup.locator(".peach-entry-name-button").first().click();187- await popup.waitForTimeout(300);188- const copyBtn = popup.locator("[aria-label*='copy' i]").first();189- await copyBtn.hover();190- await popup.waitForTimeout(500);191- await ctx.close();192- });193-});194-195-test.describe("Clipboard - Copy Username", () => {196- let ctx: BrowserContext;197- test.afterEach(async () => { await ctx.close(); });198- test.beforeEach(async () => { ctx = await makeContext(); });199-200- test("CU01 - username copy button is visible", async () => {201- const { popup } = await setupVault(ctx);202- await createEntry(popup, "UserCopyTest");203- await popup.locator(".peach-entry-name-button").first().click();204- await popup.waitForTimeout(300);205- const copyBtns = popup.locator("[aria-label*='copy' i]");206- const count = await copyBtns.count();207- expect(count).toBeGreaterThan(1);208- await ctx.close();209- });210-211- test("CU02 - copy username copies correct value", async () => {212- const { popup } = await setupVault(ctx);213- await createEntry(popup, "UserCopyTest");214- await popup.locator(".peach-entry-name-button").first().click();215- await popup.waitForTimeout(300);216- const copyBtns = popup.locator("[aria-label*='copy' i]");217- await copyBtns.nth(1).click();218- await popup.waitForTimeout(300);219- const copied = popup.locator("text=/copied|\\u2713/i");220- const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false);221- expect(hasCopied).toBeTruthy();222- await ctx.close();223- });224-225- test("CU03 - username copy shows notification", async () => {226- const { popup } = await setupVault(ctx);227- await createEntry(popup, "UserCopyTest");228- await popup.locator(".peach-entry-name-button").first().click();229- await popup.waitForTimeout(300);230- const copyBtns = popup.locator("[aria-label*='copy' i]");231- await copyBtns.nth(1).click();232- await popup.waitForTimeout(500);233- const notification = popup.locator("text=/copied/i");234- const hasNotification = await notification.isVisible().catch(() => false);235- expect(hasNotification).toBeTruthy();236- await ctx.close();237- });238-239- test("CU04 - copy username respects clipboard timeout", async () => {240- const { popup } = await setupVault(ctx);241- await createEntry(popup, "UserCopyTest");242- await popup.locator(".peach-entry-name-button").first().click();243- await popup.waitForTimeout(300);244- const copyBtns = popup.locator("[aria-label*='copy' i]");245- await copyBtns.nth(1).click();246- await popup.waitForTimeout(300);247- await ctx.close();248- });249-});250-251-test.describe("Clipboard - Settings", () => {252- let ctx: BrowserContext;253- test.afterEach(async () => { await ctx.close(); });254- test.beforeEach(async () => { ctx = await makeContext(); });255-256- test("CS01 - clipboard clear setting is available", async () => {257- const { popup } = await setupVault(ctx);258- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();259- await popup.waitForTimeout(300);260- const clipboardSetting = popup.locator("text=/clipboard|clear/i");261- const hasSetting = await clipboardSetting.isVisible().catch(() => false);262- expect(hasSetting).toBeTruthy();263- await ctx.close();264- });265-266- test("CS02 - clipboard clear time options", async () => {267- const { popup } = await setupVault(ctx);268- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();269- await popup.waitForTimeout(300);270- await popup.locator("text=/clipboard|clear/i").click();271- await popup.waitForTimeout(300);272- const options = popup.locator("[role='option' i], [role='menuitem' i]");273- const count = await options.count();274- expect(count).toBeGreaterThan(1);275- await ctx.close();276- });277-278- test("CS03 - clipboard clear 30 seconds option", async () => {279- const { popup } = await setupVault(ctx);280- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();281- await popup.waitForTimeout(300);282- await popup.locator("text=/clipboard|clear/i").click();283- await popup.waitForTimeout(300);284- const thirtySec = popup.locator("text=/30 seconds|30s/i");285- const hasThirty = await thirtySec.isVisible().catch(() => false);286- expect(hasThirty).toBeTruthy();287- await ctx.close();288- });289-290- test("CS04 - clipboard clear 1 minute option", async () => {291- const { popup } = await setupVault(ctx);292- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();293- await popup.waitForTimeout(300);294- await popup.locator("text=/clipboard|clear/i").click();295- await popup.waitForTimeout(300);296- const oneMin = popup.locator("text=/1 minute|60 seconds/i");297- const hasOneMin = await oneMin.isVisible().catch(() => false);298- expect(hasOneMin).toBeTruthy();299- await ctx.close();300- });301-302- test("CS05 - clipboard clear never option", async () => {303- const { popup } = await setupVault(ctx);304- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();305- await popup.waitForTimeout(300);306- await popup.locator("text=/clipboard|clear/i").click();307- await popup.waitForTimeout(300);308- const neverOpt = popup.locator("text=/never|disable/i");309- const hasNever = await neverOpt.isVisible().catch(() => false);310- expect(hasNever).toBeTruthy();311- await ctx.close();312- });313-314- test("CS06 - clipboard setting persists after lock", async () => {315- const { popup } = await setupVault(ctx);316- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();317- await popup.waitForTimeout(300);318- await popup.locator("text=/clipboard|clear/i").click();319- await popup.waitForTimeout(300);320- const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);321- await option.click();322- await popup.waitForTimeout(300);323- await popup.keyboard.press("Escape");324- await popup.waitForTimeout(200);325- await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();326- await popup.waitForTimeout(500);327- await popup.locator("#master-password").fill("password123");328- await popup.getByRole("button", { name: /unlock/i }).click();329- await popup.waitForTimeout(2000);330- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();331- await popup.waitForTimeout(300);332- await ctx.close();333- });334-});335-336-test.describe("Clipboard - Security", () => {337- let ctx: BrowserContext;338- test.afterEach(async () => { await ctx.close(); });339- test.beforeEach(async () => { ctx = await makeContext(); });340-341- test("CSEC01 - sensitive data not logged", async () => {342- const { popup } = await setupVault(ctx);343- await createEntry(popup, "SecurityTest");344- await popup.locator(".peach-entry-name-button").first().click();345- await popup.waitForTimeout(300);346- const copyBtn = popup.locator("[aria-label*='copy' i]").first();347- await copyBtn.click();348- await popup.waitForTimeout(300);349- await ctx.close();350- });351-352- test("CSEC02 - clipboard cleared on lock", async () => {353- const { popup } = await setupVault(ctx);354- await createEntry(popup, "SecurityTest");355- await popup.locator(".peach-entry-name-button").first().click();356- await popup.waitForTimeout(300);357- const copyBtn = popup.locator("[aria-label*='copy' i]").first();358- await copyBtn.click();359- await popup.waitForTimeout(500);360- await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();361- await popup.waitForTimeout(500);362- await ctx.close();363- });364-365- test("CSEC03 - no clipboard data in page source", async () => {366- const { popup } = await setupVault(ctx);367- await createEntry(popup, "SecurityTest");368- await popup.locator(".peach-entry-name-button").first().click();369- await popup.waitForTimeout(300);370- const copyBtn = popup.locator("[aria-label*='copy' i]").first();371- await copyBtn.click();372- await popup.waitForTimeout(300);373- const html = await popup.content();374- expect(html).not.toContain("testpass123");375- await ctx.close();376- });377-});tests/codex-pdf-restore.spec.ts+1 −2
@@ -22,7 +22,7 @@2222 * triggering handlePDFSelected() inside the extension.2323 */2424 25-import { test, expect } from "@playwright/test";25+import { chromium, test, expect } from "@playwright/test";2626 import type { BrowserContext, Page } from "@playwright/test";2727 import { fileURLToPath } from "url";2828 import { dirname, join } from "path";@@ -38,7 +38,6 @@3838 const CURRENT_GEN_PDF_PATH = join(__dirname, "fixtures", "peach-codex-current-gen.pdf");3939 4040 async function makeContext(): Promise<BrowserContext> {41- const { chromium } = await import("playwright");4241 const userDataDir = join(os.tmpdir(), `peach-codex-test-${Date.now()}`);4342 await fs.mkdir(userDataDir, { recursive: true });4443 return chromium.launchPersistentContext(userDataDir, {tests/copy-totp.spec.ts+0 −156
@@ -1,156 +0,0 @@1-import { test, expect } from "@playwright/test";2-import type { BrowserContext } from "@playwright/test";3-import { fileURLToPath } from "url";4-import { dirname, join } from "path";5-import os from "os";6-import fs from "fs/promises";7-8-const __filename = fileURLToPath(import.meta.url);9-const __dirname = dirname(__filename);10-const EXTENSION_PATH = join(__dirname, "..", "dist");11-12-async function makeContext() {13- const { chromium } = await import("playwright");14- const userDataDir = join(os.tmpdir(), `peach-test-${Date.now()}`);15- await fs.mkdir(userDataDir, { recursive: true });16- return chromium.launchPersistentContext(userDataDir, {17- headless: false,18- args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],19- });20-}21-22-async function setupVault(ctx: BrowserContext): Promise<{ popup: import("@playwright/test").Page, extId: string }> {23- const maxWait = 15000;24- const start = Date.now();25- let sw = ctx.serviceWorkers()[0];26- while (!sw && Date.now() - start < maxWait) {27- await new Promise(r => setTimeout(r, 500));28- sw = ctx.serviceWorkers()[0];29- }30- if (!sw) throw new Error(`No SW after ${maxWait}ms`);31- const extId = new URL(sw.url()).host;32- const popup = await ctx.newPage();33- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);34- await popup.waitForTimeout(2000);35- const pw = popup.locator("#master-password");36- if (await pw.isVisible()) {37- await pw.fill("password123");38- await popup.locator("#confirm-password").fill("password123");39- await popup.getByRole("button", { name: /create vault/i }).click();40- await popup.waitForTimeout(2000);41- }42- return { popup, extId };43-}44-45-test.describe("Copy & TOTP", () => {46- let ctx: BrowserContext;47- test.afterEach(async () => { await ctx.close(); });48- test.beforeEach(async () => { ctx = await makeContext(); });49-50- test("14 - copy username shows copied state", async () => {51- const { popup } = await setupVault(ctx);52- await popup.locator('input[placeholder="search vault..."]').click();53- await popup.getByRole("button", { name: /new entry/i }).click();54- await popup.locator(".peach-new-name-input").fill("CopyTest");55- await popup.locator('input[placeholder="domain.com"]').fill("https://copytest.com");56- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");57- await popup.locator(".peach-new-field-input[placeholder='password']").fill("copypass");58- await popup.getByRole("button", { name: /create/i }).click();59- await popup.waitForTimeout(1500);60- await popup.locator("text=CopyTest").first().click();61- await popup.waitForTimeout(500);62- const copyBtn = popup.locator(".peach-copy-btn").first();63- await copyBtn.click();64- await popup.waitForTimeout(300);65- await expect(copyBtn).toHaveClass(/is-copied/);66- });67-68- test("15 - password reveal toggles visibility", async () => {69- const { popup } = await setupVault(ctx);70- await popup.locator('input[placeholder="search vault..."]').click();71- await popup.getByRole("button", { name: /new entry/i }).click();72- await popup.locator(".peach-new-name-input").fill("RevealTest");73- await popup.locator('input[placeholder="domain.com"]').fill("https://revealtest.com");74- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");75- await popup.locator(".peach-new-field-input[placeholder='password']").fill("secretpassword");76- await popup.getByRole("button", { name: /create/i }).click();77- await popup.waitForTimeout(1500);78- await popup.locator("text=RevealTest").first().click();79- await popup.waitForTimeout(500);80- await expect(popup.locator(".peach-password-dots")).toBeVisible();81- const eyeBtn = popup.locator("button[aria-label='Reveal password']");82- await eyeBtn.click();83- await popup.waitForTimeout(300);84- });85-86- test("16 - TOTP countdown ring animates", async () => {87- const { popup } = await setupVault(ctx);88- await popup.locator('input[placeholder="search vault..."]').click();89- await popup.getByRole("button", { name: /new entry/i }).click();90- await popup.locator(".peach-new-name-input").fill("TOTPTest");91- await popup.locator('input[placeholder="domain.com"]').fill("https://totptest.com");92- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");93- await popup.locator(".peach-new-field-input[placeholder='password']").fill("totppass");94- await popup.locator('input[placeholder*="totp" i]').fill("JBSWY3DPEHPK3PXP");95- await popup.getByRole("button", { name: /create/i }).click();96- await popup.waitForTimeout(1500);97- await popup.locator("text=TOTPTest").first().click();98- await popup.waitForTimeout(500);99- const ring = popup.locator(".peach-totp-ring-progress, svg circle").first();100- await expect(ring).toBeVisible();101- });102-103- test("17 - generator produces different passwords each time", async () => {104- const { popup } = await setupVault(ctx);105- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();106- await popup.waitForTimeout(300);107- await popup.getByRole("button", { name: /password generator/i }).click();108- await popup.waitForTimeout(500);109- const output1 = await popup.locator(".peach-pwgen-output").textContent();110- await popup.locator("button[aria-label='Regenerate']").click();111- await popup.waitForTimeout(300);112- const output2 = await popup.locator(".peach-pwgen-output").textContent();113- expect(output1).not.toEqual(output2);114- });115-116- test("18 - generator respects length slider", async () => {117- const { popup } = await setupVault(ctx);118- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();119- await popup.waitForTimeout(300);120- await popup.getByRole("button", { name: /password generator/i }).click();121- await popup.waitForTimeout(500);122- const slider = popup.locator(".peach-pwgen-slider");123- await slider.fill("32");124- await popup.waitForTimeout(300);125- const output = await popup.locator(".peach-pwgen-output").textContent();126- expect(output?.length).toBeGreaterThanOrEqual(30);127- });128-129- test("19 - settings accent color swatches update --ac", async () => {130- const { popup } = await setupVault(ctx);131- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();132- await popup.waitForTimeout(500);133- const swatch = popup.locator(".peach-swatch").nth(2);134- await swatch.click();135- await popup.waitForTimeout(300);136- await expect(swatch).toHaveClass(/is-active/);137- });138-139- test("20 - clipboard clear after 30 seconds", async () => {140- const { popup } = await setupVault(ctx);141- await popup.locator('input[placeholder="search vault..."]').click();142- await popup.getByRole("button", { name: /new entry/i }).click();143- await popup.locator(".peach-new-name-input").fill("ClipboardTest");144- await popup.locator('input[placeholder="domain.com"]').fill("https://clipboardtest.com");145- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");146- await popup.locator(".peach-new-field-input[placeholder='password']").fill("cleartest");147- await popup.getByRole("button", { name: /create/i }).click();148- await popup.waitForTimeout(1500);149- await popup.locator("text=ClipboardTest").first().click();150- await popup.waitForTimeout(500);151- await popup.locator(".peach-copy-btn").nth(1).click();152- await popup.waitForTimeout(200);153- const copyBtns = popup.locator(".peach-copy-btn.is-copied");154- expect(await copyBtns.count()).toBeGreaterThan(0);155- });156-});\ No newline at end of filetests/entry-comprehensive.spec.ts+0 −682
@@ -1,682 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async function makeContext() {12- const { chromium } = await import("playwright");13- const userDataDir = join(os.tmpdir(), `peach-entry-${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-}20-21-async 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-}31-32-async 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-}46-47-test.describe("Entry Field Validation", () => {48- let ctx: BrowserContext;49- test.afterEach(async () => { await ctx.close(); });50- test.beforeEach(async () => { ctx = await makeContext(); });51-52- test("EF01 - entry requires name", 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('input[placeholder="domain.com"]').fill("https://noname.com");57- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");58- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");59- await popup.getByRole("button", { name: /create/i }).click();60- await popup.waitForTimeout(500);61- const nameInput = popup.locator(".peach-new-name-input");62- const value = await nameInput.inputValue();63- expect(value).toBe("");64- await ctx.close();65- });66-67- test("EF02 - entry allows empty domain", 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("NoDomain");72- await popup.locator('input[placeholder="domain.com"]').fill("");73- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");74- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");75- await popup.getByRole("button", { name: /create/i }).click();76- await popup.waitForTimeout(1500);77- await expect(popup.locator("text=NoDomain")).toBeVisible({ timeout: 3000 });78- await ctx.close();79- });80-81- test("EF03 - entry allows empty username", async () => {82- const { popup } = await setupVault(ctx);83- await popup.locator('input[placeholder="search vault..."]').click();84- await popup.getByRole("button", { name: /new entry/i }).click();85- await popup.locator(".peach-new-name-input").fill("NoUsername");86- await popup.locator('input[placeholder="domain.com"]').fill("https://nouser.com");87- await popup.locator('input[placeholder="username or email"]').fill("");88- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");89- await popup.getByRole("button", { name: /create/i }).click();90- await popup.waitForTimeout(1500);91- await expect(popup.locator("text=NoUsername")).toBeVisible({ timeout: 3000 });92- await ctx.close();93- });94-95- test("EF04 - entry requires password", async () => {96- const { popup } = await setupVault(ctx);97- await popup.locator('input[placeholder="search vault..."]').click();98- await popup.getByRole("button", { name: /new entry/i }).click();99- await popup.locator(".peach-new-name-input").fill("NoPass");100- await popup.locator('input[placeholder="domain.com"]').fill("https://nopass.com");101- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");102- await popup.getByRole("button", { name: /create/i }).click();103- await popup.waitForTimeout(500);104- const passField = popup.locator(".peach-new-field-input[placeholder='password']");105- const value = await passField.inputValue();106- expect(value).toBe("");107- await ctx.close();108- });109-110- test("EF05 - entry truncates very long names", async () => {111- const { popup } = await setupVault(ctx);112- await popup.locator('input[placeholder="search vault..."]').click();113- await popup.getByRole("button", { name: /new entry/i }).click();114- const longName = "A".repeat(500);115- await popup.locator(".peach-new-name-input").fill(longName);116- await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");117- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");118- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");119- await popup.getByRole("button", { name: /create/i }).click();120- await popup.waitForTimeout(1500);121- const nameInList = popup.locator(".peach-entry-name").first();122- const text = await nameInList.textContent();123- expect(text?.length).toBeLessThanOrEqual(200);124- await ctx.close();125- });126-});127-128-test.describe("Notes Field", () => {129- let ctx: BrowserContext;130- test.afterEach(async () => { await ctx.close(); });131- test.beforeEach(async () => { ctx = await makeContext(); });132-133- test("EF06 - notes field accepts multiline text", async () => {134- const { popup } = await setupVault(ctx);135- await popup.locator('input[placeholder="search vault..."]').click();136- await popup.getByRole("button", { name: /new entry/i }).click();137- await popup.locator(".peach-new-name-input").fill("MultiLine");138- await popup.locator('input[placeholder="domain.com"]').fill("https://multi.com");139- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");140- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");141- const notes = popup.locator("textarea");142- await notes.fill("Line 1\nLine 2\nLine 3");143- await popup.getByRole("button", { name: /create/i }).click();144- await popup.waitForTimeout(1500);145- await popup.locator(".peach-entry-name-button").first().click();146- await popup.waitForTimeout(500);147- const noteContent = popup.locator(".peach-note-content");148- await expect(noteContent).toBeVisible({ timeout: 3000 });149- const text = await noteContent.textContent();150- expect(text).toContain("Line 1");151- expect(text).toContain("Line 2");152- await ctx.close();153- });154-155- test("EF07 - notes field accepts special characters", 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("SpecialNotes");160- await popup.locator('input[placeholder="domain.com"]').fill("https://special.com");161- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");162- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");163- const notes = popup.locator("textarea");164- await notes.fill('<script>alert("xss")</script>&"\'<>@#$%^&*()_+-=[]{}|;:,./<>?');165- await popup.getByRole("button", { name: /create/i }).click();166- await popup.waitForTimeout(1500);167- await popup.locator(".peach-entry-name-button").first().click();168- await popup.waitForTimeout(500);169- const noteContent = popup.locator(".peach-note-content");170- const html = await noteContent.innerHTML();171- expect(html).not.toContain("<script>");172- await ctx.close();173- });174-175- test("EF08 - notes field supports unicode", async () => {176- const { popup } = await setupVault(ctx);177- await popup.locator('input[placeholder="search vault..."]').click();178- await popup.getByRole("button", { name: /new entry/i }).click();179- await popup.locator(".peach-new-name-input").fill("UnicodeNotes");180- await popup.locator('input[placeholder="domain.com"]').fill("https://unicode.com");181- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");182- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");183- const notes = popup.locator("textarea");184- await notes.fill("日本語한국어中文العربيةहिन्दी🎉🎊💻⌨️");185- await popup.getByRole("button", { name: /create/i }).click();186- await popup.waitForTimeout(1500);187- await expect(popup.locator("text=UnicodeNotes")).toBeVisible({ timeout: 3000 });188- await ctx.close();189- });190-191- test("EF09 - very long notes are saved", async () => {192- const { popup } = await setupVault(ctx);193- await popup.locator('input[placeholder="search vault..."]').click();194- await popup.getByRole("button", { name: /new entry/i }).click();195- await popup.locator(".peach-new-name-input").fill("LongNotes");196- await popup.locator('input[placeholder="domain.com"]').fill("https://longnotes.com");197- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");198- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");199- const notes = popup.locator("textarea");200- const longText = "A".repeat(5000);201- await notes.fill(longText);202- await popup.getByRole("button", { name: /create/i }).click();203- await popup.waitForTimeout(1500);204- await popup.locator(".peach-entry-name-button").first().click();205- await popup.waitForTimeout(500);206- const noteContent = popup.locator(".peach-note-content");207- const text = await noteContent.textContent();208- expect(text?.length).toBeGreaterThan(4000);209- await ctx.close();210- });211-212- test("EF10 - notes field shows correct label", async () => {213- const { popup } = await setupVault(ctx);214- await popup.locator('input[placeholder="search vault..."]').click();215- await popup.getByRole("button", { name: /new entry/i }).click();216- await popup.locator(".peach-new-name-input").fill("NotesLabel");217- await popup.locator('input[placeholder="domain.com"]').fill("https://label.com");218- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");219- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");220- await popup.locator("textarea").fill("test notes");221- await popup.getByRole("button", { name: /create/i }).click();222- await popup.waitForTimeout(1500);223- await popup.locator(".peach-entry-name-button").first().click();224- await popup.waitForTimeout(500);225- const label = popup.locator(".peach-label").filter({ hasText: "notes" }).first();226- await expect(label).toBeVisible({ timeout: 3000 });227- await ctx.close();228- });229-});230-231-test.describe("URL Field", () => {232- let ctx: BrowserContext;233- test.afterEach(async () => { await ctx.close(); });234- test.beforeEach(async () => { ctx = await makeContext(); });235-236- test("EF11 - URL field accepts https://", async () => {237- const { popup } = await setupVault(ctx);238- await popup.locator('input[placeholder="search vault..."]').click();239- await popup.getByRole("button", { name: /new entry/i }).click();240- await popup.locator(".peach-new-name-input").fill("HTTPSURL");241- await popup.locator('input[placeholder="domain.com"]').fill("https://https.com");242- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");243- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");244- await popup.getByRole("button", { name: /create/i }).click();245- await popup.waitForTimeout(1500);246- await expect(popup.locator("text=HTTPSURL")).toBeVisible({ timeout: 3000 });247- await ctx.close();248- });249-250- test("EF12 - URL field accepts http://", async () => {251- const { popup } = await setupVault(ctx);252- await popup.locator('input[placeholder="search vault..."]').click();253- await popup.getByRole("button", { name: /new entry/i }).click();254- await popup.locator(".peach-new-name-input").fill("HTTPURL");255- await popup.locator('input[placeholder="domain.com"]').fill("http://http.com");256- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");257- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");258- await popup.getByRole("button", { name: /create/i }).click();259- await popup.waitForTimeout(1500);260- await expect(popup.locator("text=HTTPURL")).toBeVisible({ timeout: 3000 });261- await ctx.close();262- });263-264- test("EF13 - URL field accepts ftp://", async () => {265- const { popup } = await setupVault(ctx);266- await popup.locator('input[placeholder="search vault..."]').click();267- await popup.getByRole("button", { name: /new entry/i }).click();268- await popup.locator(".peach-new-name-input").fill("FTPURL");269- await popup.locator('input[placeholder="domain.com"]').fill("ftp://ftp.com");270- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");271- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");272- await popup.getByRole("button", { name: /create/i }).click();273- await popup.waitForTimeout(1500);274- await expect(popup.locator("text=FTPURL")).toBeVisible({ timeout: 3000 });275- await ctx.close();276- });277-278- test("EF14 - URL with path is saved correctly", async () => {279- const { popup } = await setupVault(ctx);280- await popup.locator('input[placeholder="search vault..."]').click();281- await popup.getByRole("button", { name: /new entry/i }).click();282- await popup.locator(".peach-new-name-input").fill("PathURL");283- await popup.locator('input[placeholder="domain.com"]').fill("https://example.com/path/to/page?query=value#anchor");284- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");285- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");286- await popup.getByRole("button", { name: /create/i }).click();287- await popup.waitForTimeout(1500);288- await expect(popup.locator("text=PathURL")).toBeVisible({ timeout: 3000 });289- await ctx.close();290- });291-292- test("EF15 - URL with port is saved correctly", async () => {293- const { popup } = await setupVault(ctx);294- await popup.locator('input[placeholder="search vault..."]').click();295- await popup.getByRole("button", { name: /new entry/i }).click();296- await popup.locator(".peach-new-name-input").fill("PortURL");297- await popup.locator('input[placeholder="domain.com"]').fill("https://example.com:8443/login");298- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");299- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");300- await popup.getByRole("button", { name: /create/i }).click();301- await popup.waitForTimeout(1500);302- await expect(popup.locator("text=PortURL")).toBeVisible({ timeout: 3000 });303- await ctx.close();304- });305-});306-307-test.describe("Username Field", () => {308- let ctx: BrowserContext;309- test.afterEach(async () => { await ctx.close(); });310- test.beforeEach(async () => { ctx = await makeContext(); });311-312- test("EF16 - email format username", async () => {313- const { popup } = await setupVault(ctx);314- await popup.locator('input[placeholder="search vault..."]').click();315- await popup.getByRole("button", { name: /new entry/i }).click();316- await popup.locator(".peach-new-name-input").fill("EmailUser");317- await popup.locator('input[placeholder="domain.com"]').fill("https://email.com");318- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");319- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");320- await popup.getByRole("button", { name: /create/i }).click();321- await popup.waitForTimeout(1500);322- await expect(popup.locator("text=EmailUser")).toBeVisible({ timeout: 3000 });323- await ctx.close();324- });325-326- test("EF17 - username with special characters", async () => {327- const { popup } = await setupVault(ctx);328- await popup.locator('input[placeholder="search vault..."]').click();329- await popup.getByRole("button", { name: /new entry/i }).click();330- await popup.locator(".peach-new-name-input").fill("SpecialUser");331- await popup.locator('input[placeholder="domain.com"]').fill("https://special.com");332- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");333- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");334- await popup.getByRole("button", { name: /create/i }).click();335- await popup.waitForTimeout(1500);336- await expect(popup.locator("text=SpecialUser")).toBeVisible({ timeout: 3000 });337- await ctx.close();338- });339-340- test("EF18 - very long username", async () => {341- const { popup } = await setupVault(ctx);342- await popup.locator('input[placeholder="search vault..."]').click();343- await popup.getByRole("button", { name: /new entry/i }).click();344- await popup.locator(".peach-new-name-input").fill("LongUser");345- await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");346- await popup.locator('input[placeholder="username or email"]').fill("a".repeat(200) + "@test.com");347- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");348- await popup.getByRole("button", { name: /create/i }).click();349- await popup.waitForTimeout(1500);350- await expect(popup.locator("text=LongUser")).toBeVisible({ timeout: 3000 });351- await ctx.close();352- });353-354- test("EF19 - username with unicode", async () => {355- const { popup } = await setupVault(ctx);356- await popup.locator('input[placeholder="search vault..."]').click();357- await popup.getByRole("button", { name: /new entry/i }).click();358- await popup.locator(".peach-new-name-input").fill("UnicodeUser");359- await popup.locator('input[placeholder="domain.com"]').fill("https://unicode.com");360- await popup.locator('input[placeholder="username or email"]').fill("用户@例子.测试");361- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");362- await popup.getByRole("button", { name: /create/i }).click();363- await popup.waitForTimeout(1500);364- await expect(popup.locator("text=UnicodeUser")).toBeVisible({ timeout: 3000 });365- await ctx.close();366- });367-});368-369-test.describe("Password Field", () => {370- let ctx: BrowserContext;371- test.afterEach(async () => { await ctx.close(); });372- test.beforeEach(async () => { ctx = await makeContext(); });373-374- test("EF20 - emoji password", async () => {375- const { popup } = await setupVault(ctx);376- await popup.locator('input[placeholder="search vault..."]').click();377- await popup.getByRole("button", { name: /new entry/i }).click();378- await popup.locator(".peach-new-name-input").fill("EmojiPass");379- await popup.locator('input[placeholder="domain.com"]').fill("https://emoji.com");380- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");381- await popup.locator(".peach-new-field-input[placeholder='password']").fill("🔐🔑🌟💻🖥️⌨️");382- await popup.getByRole("button", { name: /create/i }).click();383- await popup.waitForTimeout(1500);384- await expect(popup.locator("text=EmojiPass")).toBeVisible({ timeout: 3000 });385- await ctx.close();386- });387-388- test("EF21 - whitespace-only password", async () => {389- const { popup } = await setupVault(ctx);390- await popup.locator('input[placeholder="search vault..."]').click();391- await popup.getByRole("button", { name: /new entry/i }).click();392- await popup.locator(".peach-new-name-input").fill("SpacePass");393- await popup.locator('input[placeholder="domain.com"]').fill("https://space.com");394- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");395- await popup.locator(".peach-new-field-input[placeholder='password']").fill(" ");396- await popup.getByRole("button", { name: /create/i }).click();397- await popup.waitForTimeout(1500);398- await expect(popup.locator("text=SpacePass")).toBeVisible({ timeout: 3000 });399- await ctx.close();400- });401-402- test("EF22 - very long password", async () => {403- const { popup } = await setupVault(ctx);404- await popup.locator('input[placeholder="search vault..."]').click();405- await popup.getByRole("button", { name: /new entry/i }).click();406- await popup.locator(".peach-new-name-input").fill("LongPass");407- await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");408- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");409- await popup.locator(".peach-new-field-input[placeholder='password']").fill("a".repeat(1000));410- await popup.getByRole("button", { name: /create/i }).click();411- await popup.waitForTimeout(1500);412- await expect(popup.locator("text=LongPass")).toBeVisible({ timeout: 3000 });413- await ctx.close();414- });415-416- test("EF23 - password reveal button works", async () => {417- const { popup } = await setupVault(ctx);418- await popup.locator('input[placeholder="search vault..."]').click();419- await popup.getByRole("button", { name: /new entry/i }).click();420- await popup.locator(".peach-new-name-input").fill("RevealPass");421- await popup.locator('input[placeholder="domain.com"]').fill("https://reveal.com");422- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");423- await popup.locator(".peach-new-field-input[placeholder='password']").fill("secretpass");424- await popup.getByRole("button", { name: /create/i }).click();425- await popup.waitForTimeout(1500);426- await popup.locator(".peach-entry-name-button").first().click();427- await popup.waitForTimeout(500);428- const eyeBtn = popup.locator("button[aria-label='Reveal password']");429- if (await eyeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {430- await eyeBtn.click();431- await popup.waitForTimeout(300);432- }433- await ctx.close();434- });435-});436-437-test.describe("Entry Editing", () => {438- let ctx: BrowserContext;439- test.afterEach(async () => { await ctx.close(); });440- test.beforeEach(async () => { ctx = await makeContext(); });441-442- test("EF24 - edit entry changes name", async () => {443- const { popup } = await setupVault(ctx);444- await popup.locator('input[placeholder="search vault..."]').click();445- await popup.getByRole("button", { name: /new entry/i }).click();446- await popup.locator(".peach-new-name-input").fill("OriginalName");447- await popup.locator('input[placeholder="domain.com"]').fill("https://original.com");448- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");449- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");450- await popup.getByRole("button", { name: /create/i }).click();451- await popup.waitForTimeout(1500);452- await popup.locator(".peach-entry-name-button").first().click();453- await popup.waitForTimeout(500);454- await popup.locator("button[aria-label='Edit']").first().click();455- await popup.waitForTimeout(500);456- await popup.locator(".peach-new-name-input").fill("ChangedName");457- await popup.getByRole("button", { name: /save/i }).click();458- await popup.waitForTimeout(1500);459- await expect(popup.locator("text=ChangedName")).toBeVisible({ timeout: 3000 });460- await ctx.close();461- });462-463- test("EF25 - edit entry preserves other fields", async () => {464- const { popup } = await setupVault(ctx);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("PreserveFields");468- await popup.locator('input[placeholder="domain.com"]').fill("https://preserve.com");469- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");470- await popup.locator(".peach-new-field-input[placeholder='password']").fill("originalpass");471- await popup.getByRole("button", { name: /create/i }).click();472- await popup.waitForTimeout(1500);473- await popup.locator(".peach-entry-name-button").first().click();474- await popup.waitForTimeout(500);475- await popup.locator("button[aria-label='Edit']").first().click();476- await popup.waitForTimeout(500);477- await popup.locator(".peach-new-name-input").fill("PreserveFieldsChanged");478- await popup.getByRole("button", { name: /save/i }).click();479- await popup.waitForTimeout(1500);480- await popup.locator(".peach-entry-name-button").first().click();481- await popup.waitForTimeout(500);482- const username = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();483- await expect(username).toBeVisible({ timeout: 3000 });484- await ctx.close();485- });486-487- test("EF26 - cancel edit reverts changes", async () => {488- const { popup } = await setupVault(ctx);489- await popup.locator('input[placeholder="search vault..."]').click();490- await popup.getByRole("button", { name: /new entry/i }).click();491- await popup.locator(".peach-new-name-input").fill("CancelEdit");492- await popup.locator('input[placeholder="domain.com"]').fill("https://cancel.com");493- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");494- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");495- await popup.getByRole("button", { name: /create/i }).click();496- await popup.waitForTimeout(1500);497- await popup.locator(".peach-entry-name-button").first().click();498- await popup.waitForTimeout(500);499- await popup.locator("button[aria-label='Edit']").first().click();500- await popup.waitForTimeout(500);501- await popup.locator(".peach-new-name-input").fill("ShouldNotChange");502- await popup.keyboard.press("Escape");503- await popup.waitForTimeout(300);504- await expect(popup.locator("text=CancelEdit")).toBeVisible({ timeout: 3000 });505- await ctx.close();506- });507-508- test("EF27 - delete entry from detail view", async () => {509- const { popup } = await setupVault(ctx);510- await popup.locator('input[placeholder="search vault..."]').click();511- await popup.getByRole("button", { name: /new entry/i }).click();512- await popup.locator(".peach-new-name-input").fill("DeleteMeEntry");513- await popup.locator('input[placeholder="domain.com"]').fill("https://delete.com");514- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");515- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");516- await popup.getByRole("button", { name: /create/i }).click();517- await popup.waitForTimeout(1500);518- await popup.locator(".peach-entry-name-button").first().click();519- await popup.waitForTimeout(500);520- const deleteLink = popup.locator("text=delete entry");521- await deleteLink.click();522- await popup.waitForTimeout(1000);523- await expect(popup.locator("text=DeleteMeEntry")).not.toBeVisible({ timeout: 3000 });524- await ctx.close();525- });526-});527-528-test.describe("Entry State Transitions", () => {529- let ctx: BrowserContext;530- test.afterEach(async () => { await ctx.close(); });531- test.beforeEach(async () => { ctx = await makeContext(); });532-533- test("EF28 - list to detail transition", async () => {534- const { popup } = await setupVault(ctx);535- await popup.locator('input[placeholder="search vault..."]').click();536- await popup.getByRole("button", { name: /new entry/i }).click();537- await popup.locator(".peach-new-name-input").fill("ListDetail");538- await popup.locator('input[placeholder="domain.com"]').fill("https://listdetail.com");539- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");540- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");541- await popup.getByRole("button", { name: /create/i }).click();542- await popup.waitForTimeout(1500);543- await popup.locator(".peach-entry-name-button").first().click();544- await popup.waitForTimeout(500);545- const usernameField = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();546- await expect(usernameField).toBeVisible({ timeout: 3000 });547- await ctx.close();548- });549-550- test("EF29 - detail to list back transition", async () => {551- const { popup } = await setupVault(ctx);552- await popup.locator('input[placeholder="search vault..."]').click();553- await popup.getByRole("button", { name: /new entry/i }).click();554- await popup.locator(".peach-new-name-input").fill("DetailBack");555- await popup.locator('input[placeholder="domain.com"]').fill("https://back.com");556- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");557- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");558- await popup.getByRole("button", { name: /create/i }).click();559- await popup.waitForTimeout(1500);560- await popup.locator(".peach-entry-name-button").first().click();561- await popup.waitForTimeout(500);562- await popup.locator("button[aria-label='Back']").first().click();563- await popup.waitForTimeout(300);564- await expect(popup.locator("text=DetailBack")).toBeVisible({ timeout: 3000 });565- await ctx.close();566- });567-568- test("EF30 - entry to edit mode transition", async () => {569- const { popup } = await setupVault(ctx);570- await popup.locator('input[placeholder="search vault..."]').click();571- await popup.getByRole("button", { name: /new entry/i }).click();572- await popup.locator(".peach-new-name-input").fill("EditMode");573- await popup.locator('input[placeholder="domain.com"]').fill("https://edit.com");574- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");575- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");576- await popup.getByRole("button", { name: /create/i }).click();577- await popup.waitForTimeout(1500);578- await popup.locator(".peach-entry-name-button").first().click();579- await popup.waitForTimeout(500);580- await popup.locator("button[aria-label='Edit']").first().click();581- await popup.waitForTimeout(500);582- const nameInput = popup.locator(".peach-new-name-input");583- await expect(nameInput).toBeVisible({ timeout: 3000 });584- await ctx.close();585- });586-587- test("EF31 - edit to detail transition after save", async () => {588- const { popup } = await setupVault(ctx);589- await popup.locator('input[placeholder="search vault..."]').click();590- await popup.getByRole("button", { name: /new entry/i }).click();591- await popup.locator(".peach-new-name-input").fill("EditSave");592- await popup.locator('input[placeholder="domain.com"]').fill("https://editsave.com");593- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");594- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");595- await popup.getByRole("button", { name: /create/i }).click();596- await popup.waitForTimeout(1500);597- await popup.locator(".peach-entry-name-button").first().click();598- await popup.waitForTimeout(500);599- await popup.locator("button[aria-label='Edit']").first().click();600- await popup.waitForTimeout(500);601- await popup.locator(".peach-new-name-input").fill("EditSaveChanged");602- await popup.getByRole("button", { name: /save/i }).click();603- await popup.waitForTimeout(1500);604- const usernameField = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();605- await expect(usernameField).toBeVisible({ timeout: 3000 });606- await ctx.close();607- });608-});609-610-test.describe("Entry List Behavior", () => {611- let ctx: BrowserContext;612- test.afterEach(async () => { await ctx.close(); });613- test.beforeEach(async () => { ctx = await makeContext(); });614-615- test("EF32 - entries sorted alphabetically", async () => {616- const { popup } = await setupVault(ctx);617- for (const name of ["Zebra", "Apple", "Mango"]) {618- await popup.locator('input[placeholder="search vault..."]').click();619- await popup.getByRole("button", { name: /new entry/i }).click();620- await popup.locator(".peach-new-name-input").fill(name);621- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);622- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);623- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");624- await popup.getByRole("button", { name: /create/i }).click();625- await popup.waitForTimeout(800);626- }627- const entries = popup.locator(".peach-entry-row-name");628- const count = await entries.count();629- expect(count).toBeGreaterThanOrEqual(3);630- await ctx.close();631- });632-633- test("EF33 - breach indicator shows for compromised entry", async () => {634- const { popup } = await setupVault(ctx);635- await popup.waitForTimeout(500);636- const breachRow = popup.locator(".peach-entry-row.is-breach, .peach-entry-row-dot");637- await breachRow.isVisible({ timeout: 2000 }).catch(() => false);638- await ctx.close();639- });640-641- test("EF34 - entry count updates after add", async () => {642- const { popup } = await setupVault(ctx);643- const counter = popup.locator(".peach-search-counter");644- const initial = await counter.textContent();645- await popup.locator('input[placeholder="search vault..."]').click();646- await popup.getByRole("button", { name: /new entry/i }).click();647- await popup.locator(".peach-new-name-input").fill("CountTest");648- await popup.locator('input[placeholder="domain.com"]').fill("https://count.com");649- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");650- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");651- await popup.getByRole("button", { name: /create/i }).click();652- await popup.waitForTimeout(1500);653- const final = await counter.textContent();654- expect(initial).not.toEqual(final);655- await ctx.close();656- });657-658- test("EF35 - next/prev navigation works", async () => {659- const { popup } = await setupVault(ctx);660- for (const name of ["FirstEntry", "SecondEntry", "ThirdEntry"]) {661- await popup.locator('input[placeholder="search vault..."]').click();662- await popup.getByRole("button", { name: /new entry/i }).click();663- await popup.locator(".peach-new-name-input").fill(name);664- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);665- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);666- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");667- await popup.getByRole("button", { name: /create/i }).click();668- await popup.waitForTimeout(800);669- }670- const nextBtn = popup.getByRole("button", { name: /next/i });671- if (await nextBtn.isVisible({ timeout: 2000 }).catch(() => false)) {672- await nextBtn.click();673- await popup.waitForTimeout(300);674- const prevBtn = popup.getByRole("button", { name: /prev/i });675- if (await prevBtn.isVisible({ timeout: 1000 }).catch(() => false)) {676- await prevBtn.click();677- await popup.waitForTimeout(300);678- }679- }680- await ctx.close();681- });682-});\ No newline at end of filetests/entry-flows-state.spec.ts+0 −396
@@ -1,396 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-const VAULT_PASSWORD = "CorrectHorseBattery42!";11-12-async function makeContext() {13- const { chromium } = await import("playwright");14- const userDataDir = join(os.tmpdir(), `peach-entries-${Date.now()}`);15- await fs.mkdir(userDataDir, { recursive: true });16- return chromium.launchPersistentContext(userDataDir, {17- headless: false,18- args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],19- });20-}21-22-async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {23- const start = Date.now();24- let sw = ctx.serviceWorkers()[0];25- while (!sw && Date.now() - start < maxWait) {26- await new Promise(r => setTimeout(r, 500));27- sw = ctx.serviceWorkers()[0];28- }29- if (!sw) throw new Error(`No SW after ${maxWait}ms`);30- return new URL(sw.url()).host;31-}32-33-async function setupVault(ctx: BrowserContext): Promise<{ popup: any, extId: string }> {34- const extId = await waitForSW(ctx);35- const popup = await ctx.newPage();36- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);37- await popup.waitForTimeout(3000);38- const pw = popup.locator("#master-password");39- if (await pw.isVisible()) {40- await pw.fill(VAULT_PASSWORD);41- await popup.locator("#confirm-password").fill(VAULT_PASSWORD);42- await popup.waitForFunction(() => {43- const submit = document.querySelector(".peach-lock-submit");44- return submit instanceof HTMLButtonElement && !submit.disabled;45- });46- await popup.getByRole("button", { name: /create vault/i }).click();47- await popup.waitForTimeout(2000);48- }49- return { popup, extId };50-}51-52-async function seedEntries(popup: any, names: string[]) {53- for (const name of names) {54- await popup.evaluate(async (entryName: string) => {55- await new Promise((resolve) => {56- chrome.runtime.sendMessage(57- {58- type: "ADD_ENTRY",59- payload: {60- type: "login",61- title: entryName,62- url: `https://${entryName.toLowerCase()}.com`,63- username: `${entryName.toLowerCase()}@test.com`,64- password: "Passphrase123!",65- },66- },67- resolve68- );69- });70- }, name);71- }72-}73-74-test.describe("Entry Interaction Flows", () => {75-76- test("FLOW01 - full add entry workflow", async () => {77- const ctx = await makeContext();78- const { popup } = await setupVault(ctx);79- await popup.locator('input[placeholder="search vault..."]').click();80- await popup.getByRole("button", { name: /new entry/i }).click();81- await popup.locator(".peach-new-name-input").fill("GitHub");82- await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");83- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");84- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");85- await pwdField.fill("supersecret123");86- await popup.getByRole("button", { name: /create/i }).click();87- await popup.waitForTimeout(1500);88- await expect(popup.locator("text=GitHub")).toBeVisible({ timeout: 3000 });89- await ctx.close();90- });91-92- test("FLOW02 - edit entry workflow", async () => {93- const ctx = await makeContext();94- const { popup } = await setupVault(ctx);95- await popup.locator('input[placeholder="search vault..."]').click();96- await popup.getByRole("button", { name: /new entry/i }).click();97- await popup.locator(".peach-new-name-input").fill("EditFlow");98- await popup.locator('input[placeholder="domain.com"]').fill("https://editflow.com");99- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");100- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");101- await pwdField.fill("oldpass");102- await popup.getByRole("button", { name: /create/i }).click();103- await popup.waitForTimeout(1500);104- await popup.locator("text=EditFlow").first().click();105- await popup.waitForTimeout(500);106- const editBtn = popup.locator("button[aria-label='Edit']").first();107- await editBtn.click();108- await popup.waitForTimeout(500);109- const newPwdField = popup.locator(".peach-new-field-input[placeholder='password']");110- await newPwdField.fill("newpass");111- await popup.getByRole("button", { name: /save/i }).click();112- await popup.waitForTimeout(1500);113- await ctx.close();114- });115-116- test("FLOW03 - delete entry workflow", async () => {117- const ctx = await makeContext();118- const { popup } = await setupVault(ctx);119- await popup.locator('input[placeholder="search vault..."]').click();120- await popup.getByRole("button", { name: /new entry/i }).click();121- await popup.locator(".peach-new-name-input").fill("DeleteFlow");122- await popup.locator('input[placeholder="domain.com"]').fill("https://deleteflow.com");123- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");124- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");125- await pwdField.fill("pass123");126- await popup.getByRole("button", { name: /create/i }).click();127- await popup.waitForTimeout(1500);128- await popup.locator("text=DeleteFlow").first().click();129- await popup.waitForTimeout(500);130- const deleteLink = popup.locator("text=delete entry");131- await deleteLink.click();132- await popup.waitForTimeout(1000);133- await ctx.close();134- });135-136- test("FLOW04 - copy username workflow", async () => {137- const ctx = await makeContext();138- const { popup } = await setupVault(ctx);139- await popup.locator('input[placeholder="search vault..."]').click();140- await popup.getByRole("button", { name: /new entry/i }).click();141- await popup.locator(".peach-new-name-input").fill("CopyUsername");142- await popup.locator('input[placeholder="domain.com"]').fill("https://copyuser.com");143- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");144- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");145- await pwdField.fill("pass123");146- await popup.getByRole("button", { name: /create/i }).click();147- await popup.waitForTimeout(1500);148- await popup.locator("text=CopyUsername").first().click();149- await popup.waitForTimeout(500);150- const copyBtn = popup.locator(".peach-copy-btn").first();151- await copyBtn.click();152- await popup.waitForTimeout(500);153- await ctx.close();154- });155-156- test("FLOW05 - reveal password workflow", async () => {157- const ctx = await makeContext();158- const { popup } = await setupVault(ctx);159- await popup.locator('input[placeholder="search vault..."]').click();160- await popup.getByRole("button", { name: /new entry/i }).click();161- await popup.locator(".peach-new-name-input").fill("RevealPwd");162- await popup.locator('input[placeholder="domain.com"]').fill("https://revealpwd.com");163- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");164- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");165- await pwdField.fill("mysecretpassword");166- await popup.getByRole("button", { name: /create/i }).click();167- await popup.waitForTimeout(1500);168- await popup.locator("text=RevealPwd").first().click();169- await popup.waitForTimeout(500);170- const eyeBtn = popup.locator("button[aria-label='Reveal password']");171- await eyeBtn.click();172- await popup.waitForTimeout(300);173- await ctx.close();174- });175-176- test("FLOW06 - add entry with TOTP workflow", async () => {177- const ctx = await makeContext();178- const { popup } = await setupVault(ctx);179- await popup.locator('input[placeholder="search vault..."]').click();180- await popup.getByRole("button", { name: /new entry/i }).click();181- await popup.locator(".peach-new-name-input").fill("GitHub2FA");182- await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");183- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");184- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");185- await pwdField.fill("secret123");186- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");187- await popup.getByRole("button", { name: /create/i }).click();188- await popup.waitForTimeout(1500);189- await expect(popup.locator("text=GitHub2FA")).toBeVisible({ timeout: 3000 });190- await ctx.close();191- });192-193- test("FLOW07 - search and find entry", async () => {194- const ctx = await makeContext();195- const { popup } = await setupVault(ctx);196- for (const name of ["Google", "Amazon", "Microsoft"]) {197- await popup.locator('input[placeholder="search vault..."]').click();198- await popup.getByRole("button", { name: /new entry/i }).click();199- await popup.locator(".peach-new-name-input").fill(name);200- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);201- await popup.locator('input[placeholder="username or email"]').fill(`user@${name.toLowerCase()}.com`);202- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");203- await pwdField.fill("pass123");204- await popup.getByRole("button", { name: /create/i }).click();205- await popup.waitForTimeout(500);206- }207- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();208- await searchInput.click();209- await searchInput.fill("Amazon");210- await popup.waitForTimeout(300);211- await expect(popup.locator("text=Amazon")).toBeVisible({ timeout: 3000 });212- await expect(popup.locator("text=Google")).not.toBeVisible();213- await ctx.close();214- });215-216- test("FLOW08 - navigate between entries", async () => {217- const ctx = await makeContext();218- const { popup } = await setupVault(ctx);219- const pageErrors: string[] = [];220- popup.on("pageerror", (error: Error) => pageErrors.push(error.message));221-222- await seedEntries(popup, ["First", "Second", "Third"]);223- await popup.reload();224- await popup.waitForTimeout(1500);225-226- const unlockInput = popup.locator("#unlock-password");227- if (await unlockInput.isVisible().catch(() => false)) {228- await unlockInput.fill(VAULT_PASSWORD);229- await popup.getByRole("button", { name: /unlock/i }).click();230- await popup.waitForTimeout(1500);231- }232-233- const nextBtn = popup.getByRole("button", { name: /next entry/i });234- const shellSearch = popup.locator(".peach-search-wrap .peach-search-input");235- const overlay = popup.locator(".peach-overlay-view");236- const overlaySearch = popup.locator(".peach-overlay-view .peach-search-input");237- const backBtn = popup.locator('.peach-overlay-view button[aria-label="Back"]');238-239- await expect(nextBtn).toBeVisible();240- await nextBtn.click();241- await shellSearch.click();242- await expect(overlay).toBeVisible();243- await expect(overlaySearch).toBeVisible();244- await expect(popup.locator(".peach-chrome")).toBeHidden();245- await expect(popup.locator(".peach-search-wrap")).toBeHidden();246- await expect247- .poll(async () => overlaySearch.evaluate((el: Element) => document.activeElement === el))248- .toBe(true);249- await backBtn.click();250- await expect(overlay).toBeHidden();251- await expect(popup.locator(".peach-chrome")).toBeVisible();252- await expect(popup.locator(".peach-search-wrap")).toBeVisible();253- expect(pageErrors).toEqual([]);254-255- await popup.waitForTimeout(300);256- await ctx.close();257- });258-259- test("FLOW09 - add entry with notes", async () => {260- const ctx = await makeContext();261- const { popup } = await setupVault(ctx);262- await popup.locator('input[placeholder="search vault..."]').click();263- await popup.getByRole("button", { name: /new entry/i }).click();264- await popup.locator(".peach-new-name-input").fill("NotesEntry");265- await popup.locator('input[placeholder="domain.com"]').fill("https://notes.com");266- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");267- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");268- await pwdField.fill("pass123");269- const notesArea = popup.locator("textarea");270- await notesArea.fill("This is a secret note with important information.");271- await popup.getByRole("button", { name: /create/i }).click();272- await popup.waitForTimeout(1500);273- await ctx.close();274- });275-276- test("FLOW10 - generator and use password", async () => {277- const ctx = await makeContext();278- const { popup } = await setupVault(ctx);279- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();280- await popup.waitForTimeout(300);281- await popup.getByRole("button", { name: /password generator/i }).click();282- await popup.waitForTimeout(500);283- const pwd1 = await popup.locator(".peach-pwgen-output").textContent();284- expect(pwd1?.length).toBeGreaterThan(0);285- await popup.locator("button[aria-label='Regenerate']").click();286- await popup.waitForTimeout(300);287- const pwd2 = await popup.locator(".peach-pwgen-output").textContent();288- expect(pwd1).not.toEqual(pwd2);289- const useBtn = popup.getByRole("button", { name: /use/i });290- await useBtn.click();291- await popup.waitForTimeout(500);292- await ctx.close();293- });294-});295-296-test.describe("State Transitions", () => {297-298- test("STATE01 - empty vault shows correct UI", async () => {299- const ctx = await makeContext();300- const { popup } = await setupVault(ctx);301- const emptyCopy = popup.locator(".peach-empty-copy, text=first entry");302- await expect(emptyCopy).toBeVisible({ timeout: 3000 });303- await ctx.close();304- });305-306- test("STATE02 - vault with entries shows entry count", async () => {307- const ctx = await makeContext();308- const { popup } = await setupVault(ctx);309- await popup.locator('input[placeholder="search vault..."]').click();310- await popup.getByRole("button", { name: /new entry/i }).click();311- await popup.locator(".peach-new-name-input").fill("CounterTest");312- await popup.locator('input[placeholder="domain.com"]').fill("https://counter.com");313- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");314- const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");315- await pwdField.fill("pass123");316- await popup.getByRole("button", { name: /create/i }).click();317- await popup.waitForTimeout(1500);318- const counter = popup.locator(".peach-search-counter");319- const counterText = await counter.textContent();320- expect(counterText).toMatch(/1 \/ 1|1 entries/);321- await ctx.close();322- });323-324- test("STATE03 - settings panel shows all options", async () => {325- const ctx = await makeContext();326- const { popup } = await setupVault(ctx);327- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();328- await popup.waitForTimeout(500);329- await expect(popup.locator("text=lock on close")).toBeVisible();330- await expect(popup.locator("text=auto-lock")).toBeVisible();331- await expect(popup.locator("text=clipboard clear")).toBeVisible();332- await ctx.close();333- });334-335- test("STATE04 - generator shows toggles", async () => {336- const ctx = await makeContext();337- const { popup } = await setupVault(ctx);338- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();339- await popup.waitForTimeout(300);340- await popup.getByRole("button", { name: /password generator/i }).click();341- await popup.waitForTimeout(500);342- const toggles = popup.locator(".peach-pwgen-toggle-row");343- expect(await toggles.count()).toBeGreaterThan(0);344- await ctx.close();345- });346-347- test("STATE05 - export panel requires password", async () => {348- const ctx = await makeContext();349- const { popup } = await setupVault(ctx);350- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();351- await popup.waitForTimeout(300);352- await popup.getByRole("button", { name: /export/i }).click();353- await popup.waitForTimeout(500);354- const exportPw = popup.locator("input[type='password']").first();355- await expect(exportPw).toBeVisible();356- await ctx.close();357- });358-359- test("STATE06 - import panel shows file input", async () => {360- const ctx = await makeContext();361- const { popup } = await setupVault(ctx);362- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();363- await popup.waitForTimeout(300);364- await popup.getByRole("button", { name: /import/i }).click();365- await popup.waitForTimeout(500);366- const fileInput = popup.locator("input[type='file']");367- await expect(fileInput).toBeAttached();368- await ctx.close();369- });370-371- test("STATE07 - accent color swatches are clickable", async () => {372- const ctx = await makeContext();373- const { popup } = await setupVault(ctx);374- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();375- await popup.waitForTimeout(500);376- const swatches = popup.locator(".peach-swatch");377- const count = await swatches.count();378- expect(count).toBeGreaterThan(0);379- await swatches.nth(0).click();380- await popup.waitForTimeout(200);381- await swatches.nth(1).click();382- await popup.waitForTimeout(200);383- await ctx.close();384- });385-386- test("STATE08 - lock screen shows branding", async () => {387- const ctx = await makeContext();388- const extId = await waitForSW(ctx);389- const popup = await ctx.newPage();390- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);391- await popup.waitForTimeout(2000);392- const wordmark = popup.locator(".wordmark, .peach-wordmark, text=peach");393- await expect(wordmark.first()).toBeVisible({ timeout: 3000 });394- await ctx.close();395- });396-});tests/helpers/vault.ts+2 −2
@@ -8,7 +8,7 @@88 * setupVault() below asserts it reached the unlocked vault, so that failure99 * mode is now a loud error instead of a silent pass.1010 */11-import { expect, type BrowserContext, type Page } from "@playwright/test";11+import { chromium, expect, type BrowserContext, type Page } from "@playwright/test";1212 import { fileURLToPath } from "url";1313 import { dirname, join } from "path";1414 import os from "os";@@ -25,13 +25,13 @@2525 export const UNLOCKED = ".peach-lock-btn[aria-label='Lock vault']";2626 2727 export async function makeContext(prefix = "peach"): Promise<BrowserContext> {28- const { chromium } = await import("playwright");2928 const userDataDir = join(3029 os.tmpdir(),3130 `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,3231 );3332 await fs.mkdir(userDataDir, { recursive: true });3433 return chromium.launchPersistentContext(userDataDir, {34+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,3535 headless: false,3636 args: [3737 `--load-extension=${EXTENSION_PATH}`,tests/import-export-robust.spec.ts+0 −685
@@ -1,685 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async function makeContext() {12- const { chromium } = await import("playwright");13- const userDataDir = join(os.tmpdir(), `peach-imp-${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-}20-21-async 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-}31-32-async 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-}46-47-test.describe("CSV Import", () => {48- let ctx: BrowserContext;49- test.afterEach(async () => { await ctx.close(); });50- test.beforeEach(async () => { ctx = await makeContext(); });51-52- test("IMP01 - CSV import accepts standard format", async () => {53- const { popup } = await setupVault(ctx);54- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();55- await popup.waitForTimeout(300);56- await popup.getByRole("button", { name: /import passwords/i }).click();57- await popup.waitForTimeout(500);58- const fileInput = popup.locator('input[type="file"]');59- const csvContent = [60- "title,username,password,url",61- "Google,[email protected],pass123,https://google.com",62- "Amazon,[email protected],pass456,https://amazon.com"63- ].join("\n");64- await fileInput.setInputFiles({65- name: "import.csv",66- mimeType: "text/csv",67- buffer: Buffer.from(csvContent)68- });69- await popup.waitForTimeout(1500);70- await popup.keyboard.press("Escape");71- await popup.waitForTimeout(300);72- const entries = popup.locator(".peach-entry-row");73- const count = await entries.count();74- expect(count).toBeGreaterThan(0);75- await ctx.close();76- });77-78- test("IMP02 - CSV import with different column order", async () => {79- const { popup } = await setupVault(ctx);80- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();81- await popup.waitForTimeout(300);82- await popup.getByRole("button", { name: /import passwords/i }).click();83- await popup.waitForTimeout(500);84- const fileInput = popup.locator('input[type="file"]');85- const csvContent = [86- "password,title,url,username",87- "secret1,Site1,https://site1.com,[email protected]",88- "secret2,Site2,https://site2.com,[email protected]"89- ].join("\n");90- await fileInput.setInputFiles({91- name: "import2.csv",92- mimeType: "text/csv",93- buffer: Buffer.from(csvContent)94- });95- await popup.waitForTimeout(1500);96- await ctx.close();97- });98-99- test("IMP03 - CSV import with BOM header", async () => {100- const { popup } = await setupVault(ctx);101- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();102- await popup.waitForTimeout(300);103- await popup.getByRole("button", { name: /import passwords/i }).click();104- await popup.waitForTimeout(500);105- const fileInput = popup.locator('input[type="file"]');106- const csvContent = "\ufefftitle,username,password,url\nTest,[email protected],pass,https://test.com";107- await fileInput.setInputFiles({108- name: "import-bom.csv",109- mimeType: "text/csv",110- buffer: Buffer.from(csvContent)111- });112- await popup.waitForTimeout(1500);113- await ctx.close();114- });115-116- test("IMP04 - CSV import with quotes in fields", async () => {117- const { popup } = await setupVault(ctx);118- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();119- await popup.waitForTimeout(300);120- await popup.getByRole("button", { name: /import passwords/i }).click();121- await popup.waitForTimeout(500);122- const fileInput = popup.locator('input[type="file"]');123- const csvContent = [124- "title,username,password,url",125- "\"Test, User\",\"[email protected]\",\"pass,\"\"123\"\"\",https://test.com"126- ].join("\n");127- await fileInput.setInputFiles({128- name: "import-quotes.csv",129- mimeType: "text/csv",130- buffer: Buffer.from(csvContent)131- });132- await popup.waitForTimeout(1500);133- await ctx.close();134- });135-136- test("IMP05 - CSV import with empty fields", async () => {137- const { popup } = await setupVault(ctx);138- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();139- await popup.waitForTimeout(300);140- await popup.getByRole("button", { name: /import passwords/i }).click();141- await popup.waitForTimeout(500);142- const fileInput = popup.locator('input[type="file"]');143- const csvContent = [144- "title,username,password,url",145- "EmptyFields,,,https://empty.com",146- ",[email protected],,"147- ].join("\n");148- await fileInput.setInputFiles({149- name: "import-empty.csv",150- mimeType: "text/csv",151- buffer: Buffer.from(csvContent)152- });153- await popup.waitForTimeout(1500);154- await ctx.close();155- });156-157- test("IMP06 - CSV import with newlines in fields", async () => {158- const { popup } = await setupVault(ctx);159- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();160- await popup.waitForTimeout(300);161- await popup.getByRole("button", { name: /import passwords/i }).click();162- await popup.waitForTimeout(500);163- const fileInput = popup.locator('input[type="file"]');164- const csvContent = [165- "title,username,password,url",166- "\"Multi\nLine\",\"[email protected]\",\"pass\",\"https://test.com\""167- ].join("\n");168- await fileInput.setInputFiles({169- name: "import-newline.csv",170- mimeType: "text/csv",171- buffer: Buffer.from(csvContent)172- });173- await popup.waitForTimeout(1500);174- await ctx.close();175- });176-177- test("IMP07 - CSV import with special characters", async () => {178- const { popup } = await setupVault(ctx);179- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();180- await popup.waitForTimeout(300);181- await popup.getByRole("button", { name: /import passwords/i }).click();182- await popup.waitForTimeout(500);183- const fileInput = popup.locator('input[type="file"]');184- const csvContent = [185- "title,username,password,url",186- "日本語,用户@例子.测试,密码,https://test.com",187- "Emoji🎉,[email protected],🔐🔑,https://emoji.com"188- ].join("\n");189- await fileInput.setInputFiles({190- name: "import-unicode.csv",191- mimeType: "text/csv",192- buffer: Buffer.from(csvContent)193- });194- await popup.waitForTimeout(1500);195- await ctx.close();196- });197-198- test("IMP08 - CSV import with 100+ entries", async () => {199- const { popup } = await setupVault(ctx);200- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();201- await popup.waitForTimeout(300);202- await popup.getByRole("button", { name: /import passwords/i }).click();203- await popup.waitForTimeout(500);204- const fileInput = popup.locator('input[type="file"]');205- const rows = ["title,username,password,url"];206- for (let i = 0; i < 100; i++) {207- rows.push(`Entry${i},user${i}@test.com,pass${i},https://entry${i}.com`);208- }209- const csvContent = rows.join("\n");210- await fileInput.setInputFiles({211- name: "import-large.csv",212- mimeType: "text/csv",213- buffer: Buffer.from(csvContent)214- });215- await popup.waitForTimeout(3000);216- await ctx.close();217- });218-219- test("IMP09 - CSV import with only required fields", async () => {220- const { popup } = await setupVault(ctx);221- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();222- await popup.waitForTimeout(300);223- await popup.getByRole("button", { name: /import passwords/i }).click();224- await popup.waitForTimeout(500);225- const fileInput = popup.locator('input[type="file"]');226- const csvContent = [227- "title,password",228- "MinimalImport,secretpass"229- ].join("\n");230- await fileInput.setInputFiles({231- name: "import-minimal.csv",232- mimeType: "text/csv",233- buffer: Buffer.from(csvContent)234- });235- await popup.waitForTimeout(1500);236- await ctx.close();237- });238-239- test("IMP10 - CSV import shows progress", async () => {240- const { popup } = await setupVault(ctx);241- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();242- await popup.waitForTimeout(300);243- await popup.getByRole("button", { name: /import passwords/i }).click();244- await popup.waitForTimeout(500);245- const fileInput = popup.locator('input[type="file"]');246- const rows = ["title,username,password,url"];247- for (let i = 0; i < 20; i++) {248- rows.push(`Entry${i},user${i}@test.com,pass${i},https://entry${i}.com`);249- }250- const csvContent = rows.join("\n");251- await fileInput.setInputFiles({252- name: "import-progress.csv",253- mimeType: "text/csv",254- buffer: Buffer.from(csvContent)255- });256- await popup.waitForTimeout(2000);257- const importStatus = popup.locator("text=importing, text=imported, text=entries");258- const hasStatus = await importStatus.isVisible({ timeout: 3000 }).catch(() => false);259- expect(hasStatus).toBeTruthy();260- await ctx.close();261- });262-});263-264-test.describe("JSON Export", () => {265- let ctx: BrowserContext;266- test.afterEach(async () => { await ctx.close(); });267- test.beforeEach(async () => { ctx = await makeContext(); });268-269- test("EXP01 - export creates valid JSON", async () => {270- const { popup } = await setupVault(ctx);271- await popup.locator('input[placeholder="search vault..."]').click();272- await popup.getByRole("button", { name: /new entry/i }).click();273- await popup.locator(".peach-new-name-input").fill("ExportJSON");274- await popup.locator('input[placeholder="domain.com"]').fill("https://export.com");275- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");276- await popup.locator(".peach-new-field-input[placeholder='password']").fill("exportpass");277- await popup.getByRole("button", { name: /create/i }).click();278- await popup.waitForTimeout(1500);279- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();280- await popup.waitForTimeout(300);281- await popup.getByRole("button", { name: /export backup/i }).click();282- await popup.waitForTimeout(500);283- const exportPw = popup.locator(".form-input[type='password']");284- await exportPw.fill("exportpassword");285- const downloadBtn = popup.getByRole("button", { name: /download backup/i });286- await downloadBtn.click();287- await popup.waitForTimeout(2000);288- const result = popup.locator("text=exported");289- const hasResult = await result.isVisible({ timeout: 3000 }).catch(() => false);290- expect(hasResult).toBeTruthy();291- await ctx.close();292- });293-294- test("EXP02 - export includes all entries", async () => {295- const { popup } = await setupVault(ctx);296- for (const name of ["Export1", "Export2", "Export3"]) {297- await popup.locator('input[placeholder="search vault..."]').click();298- await popup.getByRole("button", { name: /new entry/i }).click();299- await popup.locator(".peach-new-name-input").fill(name);300- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);301- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);302- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");303- await popup.getByRole("button", { name: /create/i }).click();304- await popup.waitForTimeout(800);305- }306- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();307- await popup.waitForTimeout(300);308- await popup.getByRole("button", { name: /export backup/i }).click();309- await popup.waitForTimeout(500);310- const exportPw = popup.locator(".form-input[type='password']");311- await exportPw.fill("exportpassword");312- await popup.getByRole("button", { name: /download backup/i }).click();313- await popup.waitForTimeout(2000);314- const result = popup.locator("text=3 entries").first();315- const hasResult = await result.isVisible({ timeout: 3000 }).catch(() => false);316- expect(hasResult).toBeTruthy();317- await ctx.close();318- });319-320- test("EXP03 - export with TOTP entries includes TOTP", async () => {321- const { popup } = await setupVault(ctx);322- await popup.locator('input[placeholder="search vault..."]').click();323- await popup.getByRole("button", { name: /new entry/i }).click();324- await popup.locator(".peach-new-name-input").fill("ExportTOTP");325- await popup.locator('input[placeholder="domain.com"]').fill("https://exporttotp.com");326- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");327- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");328- await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");329- await popup.getByRole("button", { name: /create/i }).click();330- await popup.waitForTimeout(1500);331- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();332- await popup.waitForTimeout(300);333- await popup.getByRole("button", { name: /export backup/i }).click();334- await popup.waitForTimeout(500);335- const exportPw = popup.locator(".form-input[type='password']");336- await exportPw.fill("exportpassword");337- await popup.getByRole("button", { name: /download backup/i }).click();338- await popup.waitForTimeout(2000);339- await ctx.close();340- });341-342- test("EXP04 - export requires password", async () => {343- const { popup } = await setupVault(ctx);344- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();345- await popup.waitForTimeout(300);346- await popup.getByRole("button", { name: /export backup/i }).click();347- await popup.waitForTimeout(500);348- const downloadBtn = popup.getByRole("button", { name: /download backup/i });349- await downloadBtn.click();350- await popup.waitForTimeout(500);351- const pwInput = popup.locator(".form-input[type='password']");352- const hasError = await pwInput.isVisible({ timeout: 2000 }).catch(() => false);353- expect(hasError).toBeTruthy();354- await ctx.close();355- });356-357- test("EXP05 - export with short password shows warning", async () => {358- const { popup } = await setupVault(ctx);359- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();360- await popup.waitForTimeout(300);361- await popup.getByRole("button", { name: /export backup/i }).click();362- await popup.waitForTimeout(500);363- const exportPw = popup.locator(".form-input[type='password']");364- await exportPw.fill("short");365- const downloadBtn = popup.getByRole("button", { name: /download backup/i });366- const isDisabled = await downloadBtn.isDisabled();367- expect(isDisabled).toBeTruthy();368- await ctx.close();369- });370-371- test("EXP06 - export empty vault works", async () => {372- const { popup } = await setupVault(ctx);373- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();374- await popup.waitForTimeout(300);375- await popup.getByRole("button", { name: /export backup/i }).click();376- await popup.waitForTimeout(500);377- const exportPw = popup.locator(".form-input[type='password']");378- await exportPw.fill("exportpassword");379- await popup.getByRole("button", { name: /download backup/i }).click();380- await popup.waitForTimeout(2000);381- const result = popup.locator("text=0 entries").first();382- const hasResult = await result.isVisible({ timeout: 3000 }).catch(() => false);383- expect(hasResult).toBeTruthy();384- await ctx.close();385- });386-});387-388-test.describe("JSON Import", () => {389- let ctx: BrowserContext;390- test.afterEach(async () => { await ctx.close(); });391- test.beforeEach(async () => { ctx = await makeContext(); });392-393- test("JIP01 - JSON import accepts Peach export format", async () => {394- const { popup } = await setupVault(ctx);395- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();396- await popup.waitForTimeout(300);397- await popup.getByRole("button", { name: /import passwords/i }).click();398- await popup.waitForTimeout(500);399- const fileInput = popup.locator('input[type="file"]');400- const jsonContent = JSON.stringify({401- version: 1,402- type: "peach-export",403- kdf: "PBKDF2-SHA256",404- entries: [405- { title: "JSONImport", username: "[email protected]", password: "pass123", url: "https://test.com" }406- ]407- });408- await fileInput.setInputFiles({409- name: "peach-export.json",410- mimeType: "application/json",411- buffer: Buffer.from(jsonContent)412- });413- await popup.waitForTimeout(1500);414- await ctx.close();415- });416-417- test("JIP02 - JSON import with 1Password format", async () => {418- const { popup } = await setupVault(ctx);419- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();420- await popup.waitForTimeout(300);421- await popup.getByRole("button", { name: /import passwords/i }).click();422- await popup.waitForTimeout(500);423- const fileInput = popup.locator('input[type="file"]');424- const json1pContent = JSON.stringify({425- accounts: [{426- vaults: [{427- items: [{428- overview: { title: "1P Import", url: "https://1p.com" },429- details: {430- loginFields: [431- { designation: "username", value: "testuser" },432- { designation: "password", value: "testpass" }433- ]434- }435- }]436- }]437- }]438- });439- await fileInput.setInputFiles({440- name: "1p-export.json",441- mimeType: "application/json",442- buffer: Buffer.from(json1pContent)443- });444- await popup.waitForTimeout(1500);445- await ctx.close();446- });447-448- test("JIP03 - JSON import with Bitwarden format", async () => {449- const { popup } = await setupVault(ctx);450- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();451- await popup.waitForTimeout(300);452- await popup.getByRole("button", { name: /import passwords/i }).click();453- await popup.waitForTimeout(500);454- const fileInput = popup.locator('input[type="file"]');455- const jsonBWContent = JSON.stringify({456- items: [457- { name: "BW Import", login: { username: "bwuser", password: "bwpass", uris: [{ uri: "https://bw.com" }] } }458- ]459- });460- await fileInput.setInputFiles({461- name: "bw-export.json",462- mimeType: "application/json",463- buffer: Buffer.from(jsonBWContent)464- });465- await popup.waitForTimeout(1500);466- await ctx.close();467- });468-469- test("JIP04 - JSON import with nested structure", async () => {470- const { popup } = await setupVault(ctx);471- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();472- await popup.waitForTimeout(300);473- await popup.getByRole("button", { name: /import passwords/i }).click();474- await popup.waitForTimeout(500);475- const fileInput = popup.locator('input[type="file"]');476- const jsonContent = JSON.stringify({477- data: {478- folders: [],479- items: [480- { id: "1", name: "Nested", login: { username: "[email protected]", password: "pass123" } }481- ]482- }483- });484- await fileInput.setInputFiles({485- name: "nested.json",486- mimeType: "application/json",487- buffer: Buffer.from(jsonContent)488- });489- await popup.waitForTimeout(1500);490- await ctx.close();491- });492-493- test("JIP05 - JSON import with TOTP secrets", async () => {494- const { popup } = await setupVault(ctx);495- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();496- await popup.waitForTimeout(300);497- await popup.getByRole("button", { name: /import passwords/i }).click();498- await popup.waitForTimeout(500);499- const fileInput = popup.locator('input[type="file"]');500- const jsonContent = JSON.stringify({501- items: [502- { name: "TOTPImport", login: { username: "[email protected]", password: "pass123", totp: "JBSWY3DPEHPK3PXP" } }503- ]504- });505- await fileInput.setInputFiles({506- name: "totp-import.json",507- mimeType: "application/json",508- buffer: Buffer.from(jsonContent)509- });510- await popup.waitForTimeout(1500);511- await ctx.close();512- });513-514- test("JIP06 - JSON import with custom fields", async () => {515- const { popup } = await setupVault(ctx);516- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();517- await popup.waitForTimeout(300);518- await popup.getByRole("button", { name: /import passwords/i }).click();519- await popup.waitForTimeout(500);520- const fileInput = popup.locator('input[type="file"]');521- const jsonContent = JSON.stringify({522- items: [523- {524- name: "CustomFields",525- login: { username: "[email protected]", password: "pass123" },526- extra: { notes: "some notes", favorite: true }527- }528- ]529- });530- await fileInput.setInputFiles({531- name: "custom-fields.json",532- mimeType: "application/json",533- buffer: Buffer.from(jsonContent)534- });535- await popup.waitForTimeout(1500);536- await ctx.close();537- });538-539- test("JIP07 - JSON import validates file format", async () => {540- const { popup } = await setupVault(ctx);541- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();542- await popup.waitForTimeout(300);543- await popup.getByRole("button", { name: /import passwords/i }).click();544- await popup.waitForTimeout(500);545- const fileInput = popup.locator('input[type="file"]');546- await fileInput.setInputFiles({547- name: "invalid.json",548- mimeType: "application/json",549- buffer: Buffer.from("not valid json {")550- });551- await popup.waitForTimeout(500);552- const error = popup.locator("text=error, text=invalid, text=parse");553- const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);554- expect(hasError).toBeTruthy();555- await ctx.close();556- });557-});558-559-test.describe("Import/Export Edge Cases", () => {560- let ctx: BrowserContext;561- test.afterEach(async () => { await ctx.close(); });562- test.beforeEach(async () => { ctx = await makeContext(); });563-564- test("IEC01 - import file larger than 10MB shows error", async () => {565- const { popup } = await setupVault(ctx);566- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();567- await popup.waitForTimeout(300);568- await popup.getByRole("button", { name: /import passwords/i }).click();569- await popup.waitForTimeout(500);570- const fileInput = popup.locator('input[type="file"]');571- const largeContent = "x".repeat(11 * 1024 * 1024);572- await fileInput.setInputFiles({573- name: "large.csv",574- mimeType: "text/csv",575- buffer: Buffer.from(largeContent)576- });577- await popup.waitForTimeout(500);578- const error = popup.locator("text=too large, text=file size, text=max");579- const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);580- expect(hasError).toBeTruthy();581- await ctx.close();582- });583-584- test("IEC02 - export to existing file prompts overwrite", async () => {585- const { popup } = await setupVault(ctx);586- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();587- await popup.waitForTimeout(300);588- await popup.getByRole("button", { name: /export backup/i }).click();589- await popup.waitForTimeout(500);590- const exportPw = popup.locator(".form-input[type='password']");591- await exportPw.fill("exportpassword");592- await popup.getByRole("button", { name: /download backup/i }).click();593- await popup.waitForTimeout(2000);594- await ctx.close();595- });596-597- test("IEC03 - import with duplicate entries merges", async () => {598- const { popup } = await setupVault(ctx);599- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();600- await popup.waitForTimeout(300);601- await popup.getByRole("button", { name: /import passwords/i }).click();602- await popup.waitForTimeout(500);603- const fileInput = popup.locator('input[type="file"]');604- const csvContent = [605- "title,username,password,url",606- "Duplicate,[email protected],pass,https://dup.com",607- "Duplicate,[email protected],pass,https://dup.com"608- ].join("\n");609- await fileInput.setInputFiles({610- name: "duplicate.csv",611- mimeType: "text/csv",612- buffer: Buffer.from(csvContent)613- });614- await popup.waitForTimeout(1500);615- const entries = popup.locator(".peach-entry-row");616- const count = await entries.count();617- expect(count).toBeLessThanOrEqual(2);618- await ctx.close();619- });620-621- test("IEC04 - export preserves entry order", async () => {622- const { popup } = await setupVault(ctx);623- const order = ["First", "Second", "Third"];624- for (const name of order) {625- await popup.locator('input[placeholder="search vault..."]').click();626- await popup.getByRole("button", { name: /new entry/i }).click();627- await popup.locator(".peach-new-name-input").fill(name);628- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);629- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);630- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");631- await popup.getByRole("button", { name: /create/i }).click();632- await popup.waitForTimeout(800);633- }634- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();635- await popup.waitForTimeout(300);636- await popup.getByRole("button", { name: /export backup/i }).click();637- await popup.waitForTimeout(500);638- const exportPw = popup.locator(".form-input[type='password']");639- await exportPw.fill("exportpassword");640- await popup.getByRole("button", { name: /download backup/i }).click();641- await popup.waitForTimeout(2000);642- await ctx.close();643- });644-645- test("IEC05 - import with missing password field", async () => {646- const { popup } = await setupVault(ctx);647- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();648- await popup.waitForTimeout(300);649- await popup.getByRole("button", { name: /import passwords/i }).click();650- await popup.waitForTimeout(500);651- const fileInput = popup.locator('input[type="file"]');652- const csvContent = "title,username\nNoPassword,[email protected]";653- await fileInput.setInputFiles({654- name: "no-password.csv",655- mimeType: "text/csv",656- buffer: Buffer.from(csvContent)657- });658- await popup.waitForTimeout(1500);659- await ctx.close();660- });661-662- test("IEC06 - export with unicode entries", async () => {663- const { popup } = await setupVault(ctx);664- await popup.locator('input[placeholder="search vault..."]').click();665- await popup.getByRole("button", { name: /new entry/i }).click();666- await popup.locator(".peach-new-name-input").fill("日本語エントリー");667- await popup.locator('input[placeholder="domain.com"]').fill("https://日本語.com");668- await popup.locator('input[placeholder="username or email"]').fill("ユーザー@例子.测试");669- await popup.locator(".peach-new-field-input[placeholder='password']").fill("密码🔐");670- await popup.getByRole("button", { name: /create/i }).click();671- await popup.waitForTimeout(1500);672- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();673- await popup.waitForTimeout(300);674- await popup.getByRole("button", { name: /export backup/i }).click();675- await popup.waitForTimeout(500);676- const exportPw = popup.locator(".form-input[type='password']");677- await exportPw.fill("exportpassword");678- await popup.getByRole("button", { name: /download backup/i }).click();679- await popup.waitForTimeout(2000);680- const result = popup.locator("text=1 entries").first();681- const hasResult = await result.isVisible({ timeout: 3000 }).catch(() => false);682- expect(hasResult).toBeTruthy();683- await ctx.close();684- });685-});\ No newline at end of filetests/keyboard-comprehensive.spec.ts+0 −787
@@ -1,787 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async function makeContext() {12- const { chromium } = await import("playwright");13- const userDataDir = join(os.tmpdir(), `peach-kb-${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-}20-21-async 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-}31-32-async 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-}46-47-test.describe("Global Keyboard Shortcuts", () => {48- let ctx: BrowserContext;49- test.afterEach(async () => { await ctx.close(); });50- test.beforeEach(async () => { ctx = await makeContext(); });51-52- test("KBG01 - Enter submits master password", async () => {53- const { popup: _popup } = await setupVault(ctx);54- const newPage = await ctx.newPage();55- const extId = await waitForSW(ctx);56- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);57- await newPage.waitForTimeout(2000);58- await newPage.locator("#master-password").fill("password123");59- await newPage.keyboard.press("Enter");60- await newPage.waitForTimeout(2000);61- const entries = newPage.locator("[class*=entry]");62- await expect(entries.first()).toBeVisible({ timeout: 5000 });63- await ctx.close();64- });65-66- test("KBG02 - Ctrl+Shift+P opens extension popup", async () => {67- const { popup } = await setupVault(ctx);68- const isVisible = await popup.isVisible();69- expect(isVisible).toBe(true);70- await ctx.close();71- });72-73- test("KBG03 - Escape from lock screen does nothing", async () => {74- const { popup: _popup } = await setupVault(ctx);75- const newPage = await ctx.newPage();76- const extId = await waitForSW(ctx);77- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);78- await newPage.waitForTimeout(2000);79- await newPage.locator("#master-password").fill("password123");80- await newPage.keyboard.press("Escape");81- await newPage.waitForTimeout(500);82- const pw = newPage.locator("#master-password");83- const isVisible = await pw.isVisible({ timeout: 2000 }).catch(() => false);84- expect(isVisible).toBe(true);85- await ctx.close();86- });87-88- test("KBG04 - Ctrl+N opens new entry dialog", async () => {89- const { popup } = await setupVault(ctx);90- await popup.keyboard.press("Control+n");91- await popup.waitForTimeout(500);92- const newBody = popup.locator(".peach-new-body");93- const isVisible = await newBody.isVisible({ timeout: 3000 }).catch(() => false);94- expect(isVisible).toBeTruthy();95- await ctx.close();96- });97-98- test("KBG05 - Ctrl+F focuses search", async () => {99- const { popup } = await setupVault(ctx);100- await popup.keyboard.press("Control+f");101- await popup.waitForTimeout(200);102- const focused = await popup.evaluate(() => document.activeElement?.className);103- expect(focused).toMatch(/search/i);104- await ctx.close();105- });106-107- test("KBG06 - Ctrl+, opens settings", async () => {108- const { popup } = await setupVault(ctx);109- await popup.keyboard.press("Control+,");110- await popup.waitForTimeout(500);111- const settingsTitle = popup.locator(".peach-overlay-title, text=settings").first();112- const isVisible = await settingsTitle.isVisible({ timeout: 3000 }).catch(() => false);113- expect(isVisible).toBeTruthy();114- await ctx.close();115- });116-117- test("KBG07 - Ctrl+L locks vault", async () => {118- const { popup } = await setupVault(ctx);119- await popup.keyboard.press("Control+l");120- await popup.waitForTimeout(1500);121- const pw = popup.locator("#master-password");122- const isVisible = await pw.isVisible({ timeout: 3000 }).catch(() => false);123- expect(isVisible).toBeTruthy();124- await ctx.close();125- });126-});127-128-test.describe("Entry Form Keyboard Navigation", () => {129- let ctx: BrowserContext;130- test.afterEach(async () => { await ctx.close(); });131- test.beforeEach(async () => { ctx = await makeContext(); });132-133- test("KBE01 - Tab moves through add entry form", async () => {134- const { popup } = await setupVault(ctx);135- await popup.locator('input[placeholder="search vault..."]').click();136- await popup.getByRole("button", { name: /new entry/i }).click();137- await popup.waitForTimeout(300);138- const nameInput = popup.locator(".peach-new-name-input");139- await nameInput.focus();140- await popup.keyboard.press("Tab");141- await popup.waitForTimeout(100);142- const focused = await popup.evaluate(() => document.activeElement?.tagName);143- expect(focused).toBe("INPUT");144- await ctx.close();145- });146-147- test("KBE02 - Tab from name goes to domain", async () => {148- const { popup } = await setupVault(ctx);149- await popup.locator('input[placeholder="search vault..."]').click();150- await popup.getByRole("button", { name: /new entry/i }).click();151- await popup.waitForTimeout(300);152- await popup.locator(".peach-new-name-input").focus();153- await popup.keyboard.press("Tab");154- await popup.waitForTimeout(100);155- const focusedPlaceholder = await popup.evaluate(() => (document.activeElement as HTMLInputElement)?.placeholder);156- expect(focusedPlaceholder).toContain("domain");157- await ctx.close();158- });159-160- test("KBE03 - Tab from domain goes to username", async () => {161- const { popup } = await setupVault(ctx);162- await popup.locator('input[placeholder="search vault..."]').click();163- await popup.getByRole("button", { name: /new entry/i }).click();164- await popup.waitForTimeout(300);165- await popup.locator(".peach-new-name-input").focus();166- await popup.keyboard.press("Tab");167- await popup.keyboard.press("Tab");168- await popup.waitForTimeout(100);169- const focusedPlaceholder = await popup.evaluate(() => (document.activeElement as HTMLInputElement)?.placeholder);170- expect(focusedPlaceholder).toContain("username");171- await ctx.close();172- });173-174- test("KBE04 - Tab from username goes to password", async () => {175- const { popup } = await setupVault(ctx);176- await popup.locator('input[placeholder="search vault..."]').click();177- await popup.getByRole("button", { name: /new entry/i }).click();178- await popup.waitForTimeout(300);179- await popup.locator(".peach-new-name-input").focus();180- await popup.keyboard.press("Tab");181- await popup.keyboard.press("Tab");182- await popup.keyboard.press("Tab");183- await popup.waitForTimeout(100);184- const focusedClass = await popup.evaluate(() => document.activeElement?.className);185- expect(focusedClass).toContain("peach-new-field-input");186- await ctx.close();187- });188-189- test("KBE05 - Shift+Tab goes backwards", async () => {190- const { popup } = await setupVault(ctx);191- await popup.locator('input[placeholder="search vault..."]').click();192- await popup.getByRole("button", { name: /new entry/i }).click();193- await popup.waitForTimeout(300);194- const pwField = popup.locator(".peach-new-field-input[placeholder='password']");195- await pwField.focus();196- await popup.keyboard.press("Shift+Tab");197- await popup.waitForTimeout(100);198- const focused = await popup.evaluate(() => document.activeElement?.tagName);199- expect(focused).toBe("INPUT");200- await ctx.close();201- });202-203- test("KBE06 - Enter submits add entry form", async () => {204- const { popup } = await setupVault(ctx);205- await popup.locator('input[placeholder="search vault..."]').click();206- await popup.getByRole("button", { name: /new entry/i }).click();207- await popup.waitForTimeout(300);208- await popup.locator(".peach-new-name-input").fill("EnterSubmit");209- await popup.locator('input[placeholder="domain.com"]').fill("https://enter.com");210- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");211- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");212- await popup.keyboard.press("Enter");213- await popup.waitForTimeout(1500);214- await expect(popup.locator("text=EnterSubmit")).toBeVisible({ timeout: 3000 });215- await ctx.close();216- });217-218- test("KBE07 - Escape closes add entry dialog", async () => {219- const { popup } = await setupVault(ctx);220- await popup.locator('input[placeholder="search vault..."]').click();221- await popup.getByRole("button", { name: /new entry/i }).click();222- await popup.waitForTimeout(300);223- await popup.keyboard.press("Escape");224- await popup.waitForTimeout(300);225- const newBody = popup.locator(".peach-new-body");226- const isHidden = await newBody.isHidden().catch(() => false);227- expect(isHidden).toBeTruthy();228- await ctx.close();229- });230-});231-232-test.describe("Vault List Keyboard Navigation", () => {233- let ctx: BrowserContext;234- test.afterEach(async () => { await ctx.close(); });235- test.beforeEach(async () => { ctx = await makeContext(); });236-237- test("KBV01 - ArrowDown moves selection down", async () => {238- const { popup } = await setupVault(ctx);239- for (const name of ["First", "Second", "Third"]) {240- await popup.locator('input[placeholder="search vault..."]').click();241- await popup.getByRole("button", { name: /new entry/i }).click();242- await popup.locator(".peach-new-name-input").fill(name);243- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);244- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);245- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");246- await popup.getByRole("button", { name: /create/i }).click();247- await popup.waitForTimeout(800);248- }249- await popup.locator('input[placeholder="search vault..."]').click();250- await popup.waitForTimeout(200);251- await popup.keyboard.press("ArrowDown");252- await popup.waitForTimeout(200);253- const activeRow = popup.locator(".peach-entry-row.is-active");254- const hasActive = await activeRow.isVisible({ timeout: 2000 }).catch(() => false);255- expect(hasActive).toBeTruthy();256- await ctx.close();257- });258-259- test("KBV02 - ArrowUp moves selection up", async () => {260- const { popup } = await setupVault(ctx);261- for (const name of ["RowUp1", "RowUp2"]) {262- await popup.locator('input[placeholder="search vault..."]').click();263- await popup.getByRole("button", { name: /new entry/i }).click();264- await popup.locator(".peach-new-name-input").fill(name);265- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);266- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);267- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");268- await popup.getByRole("button", { name: /create/i }).click();269- await popup.waitForTimeout(800);270- }271- await popup.locator('input[placeholder="search vault..."]').click();272- await popup.waitForTimeout(200);273- await popup.keyboard.press("ArrowDown");274- await popup.keyboard.press("ArrowDown");275- await popup.waitForTimeout(200);276- await popup.keyboard.press("ArrowUp");277- await popup.waitForTimeout(200);278- const activeRow = popup.locator(".peach-entry-row.is-active");279- const hasActive = await activeRow.isVisible({ timeout: 2000 }).catch(() => false);280- expect(hasActive).toBeTruthy();281- await ctx.close();282- });283-284- test("KBV03 - Enter opens selected entry", async () => {285- const { popup } = await setupVault(ctx);286- await popup.locator('input[placeholder="search vault..."]').click();287- await popup.getByRole("button", { name: /new entry/i }).click();288- await popup.locator(".peach-new-name-input").fill("OpenMe");289- await popup.locator('input[placeholder="domain.com"]').fill("https://open.com");290- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");291- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");292- await popup.getByRole("button", { name: /create/i }).click();293- await popup.waitForTimeout(1500);294- await popup.locator('input[placeholder="search vault..."]').click();295- await popup.waitForTimeout(200);296- await popup.keyboard.press("Enter");297- await popup.waitForTimeout(500);298- const usernameField = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();299- await expect(usernameField).toBeVisible({ timeout: 3000 });300- await ctx.close();301- });302-303- test("KBV04 - Home key goes to first entry", async () => {304- const { popup } = await setupVault(ctx);305- for (const name of ["AlphaHome", "BetaHome"]) {306- await popup.locator('input[placeholder="search vault..."]').click();307- await popup.getByRole("button", { name: /new entry/i }).click();308- await popup.locator(".peach-new-name-input").fill(name);309- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);310- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);311- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");312- await popup.getByRole("button", { name: /create/i }).click();313- await popup.waitForTimeout(800);314- }315- await popup.locator('input[placeholder="search vault..."]').click();316- await popup.keyboard.press("End");317- await popup.waitForTimeout(200);318- await popup.keyboard.press("Home");319- await popup.waitForTimeout(200);320- await ctx.close();321- });322-323- test("KBV05 - End key goes to last entry", async () => {324- const { popup } = await setupVault(ctx);325- for (const name of ["FirstEnd", "SecondEnd", "ThirdEnd"]) {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(name);329- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);330- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);331- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");332- await popup.getByRole("button", { name: /create/i }).click();333- await popup.waitForTimeout(800);334- }335- await popup.locator('input[placeholder="search vault..."]').click();336- await popup.keyboard.press("End");337- await popup.waitForTimeout(200);338- await ctx.close();339- });340-341- test("KBV06 - Delete key triggers delete confirmation", async () => {342- const { popup } = await setupVault(ctx);343- await popup.locator('input[placeholder="search vault..."]').click();344- await popup.getByRole("button", { name: /new entry/i }).click();345- await popup.locator(".peach-new-name-input").fill("DeleteKey");346- await popup.locator('input[placeholder="domain.com"]').fill("https://delkey.com");347- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");348- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");349- await popup.getByRole("button", { name: /create/i }).click();350- await popup.waitForTimeout(1500);351- await ctx.close();352- });353-354- test("KBV07 - Ctrl+A selects all in search", async () => {355- const { popup } = await setupVault(ctx);356- await popup.locator('input[placeholder="search vault..."]').click();357- await popup.locator('input[placeholder="search vault..."]').fill("test");358- await popup.waitForTimeout(100);359- await popup.keyboard.press("Control+a");360- await popup.waitForTimeout(100);361- const selected = await popup.evaluate(() => {362- const input = document.activeElement as HTMLInputElement;363- return input?.selectionStart === 0 && input?.selectionEnd === input?.value?.length;364- });365- expect(selected).toBeTruthy();366- await ctx.close();367- });368-});369-370-test.describe("Settings Panel Keyboard", () => {371- let ctx: BrowserContext;372- test.afterEach(async () => { await ctx.close(); });373- test.beforeEach(async () => { ctx = await makeContext(); });374-375- test("KBS01 - Tab navigates settings options", async () => {376- const { popup } = await setupVault(ctx);377- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();378- await popup.waitForTimeout(500);379- await popup.keyboard.press("Tab");380- await popup.waitForTimeout(100);381- const focused = await popup.evaluate(() => document.activeElement?.tagName);382- expect(focused).toBeTruthy();383- await ctx.close();384- });385-386- test("KBS02 - Arrow keys navigate setting options", async () => {387- const { popup } = await setupVault(ctx);388- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();389- await popup.waitForTimeout(500);390- await popup.locator("text=auto-lock").click();391- await popup.waitForTimeout(300);392- await popup.keyboard.press("ArrowDown");393- await popup.waitForTimeout(200);394- await popup.keyboard.press("Escape");395- await ctx.close();396- });397-398- test("KBS03 - Escape closes settings panel", async () => {399- const { popup } = await setupVault(ctx);400- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();401- await popup.waitForTimeout(500);402- await popup.keyboard.press("Escape");403- await popup.waitForTimeout(300);404- const overlay = popup.locator(".peach-overlay-view");405- const isHidden = await overlay.isHidden().catch(() => true);406- expect(isHidden).toBeTruthy();407- await ctx.close();408- });409-410- test("KBS04 - Enter activates focused option", async () => {411- const { popup } = await setupVault(ctx);412- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();413- await popup.waitForTimeout(500);414- const lockVault = popup.locator("text=lock vault").first();415- if (await lockVault.isVisible({ timeout: 2000 }).catch(() => false)) {416- await lockVault.focus();417- await popup.keyboard.press("Enter");418- await popup.waitForTimeout(1000);419- const pwField = popup.locator("#master-password");420- const isVisible = await pwField.isVisible({ timeout: 3000 }).catch(() => false);421- expect(isVisible).toBeTruthy();422- }423- await ctx.close();424- });425-426- test("KBS05 - Space activates toggle switches", async () => {427- const { popup } = await setupVault(ctx);428- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();429- await popup.waitForTimeout(500);430- await popup.locator("text=lock on close").click();431- await popup.waitForTimeout(300);432- const toggle = popup.locator("[role='switch' i]").first();433- if (await toggle.isVisible({ timeout: 2000 }).catch(() => false)) {434- await toggle.focus();435- await popup.keyboard.press(" ");436- await popup.waitForTimeout(200);437- }438- await ctx.close();439- });440-});441-442-test.describe("Password Generator Keyboard", () => {443- let ctx: BrowserContext;444- test.afterEach(async () => { await ctx.close(); });445- test.beforeEach(async () => { ctx = await makeContext(); });446-447- test("KBG01 - Tab navigates generator options", async () => {448- const { popup } = await setupVault(ctx);449- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();450- await popup.waitForTimeout(300);451- await popup.getByRole("button", { name: /password generator/i }).click();452- await popup.waitForTimeout(500);453- await popup.keyboard.press("Tab");454- await popup.waitForTimeout(100);455- const focused = await popup.evaluate(() => document.activeElement?.tagName);456- expect(focused).toBeTruthy();457- await ctx.close();458- });459-460- test("KBG02 - Arrow keys adjust length slider", async () => {461- const { popup } = await setupVault(ctx);462- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();463- await popup.waitForTimeout(300);464- await popup.getByRole("button", { name: /password generator/i }).click();465- await popup.waitForTimeout(500);466- const slider = popup.locator(".peach-pwgen-slider");467- await slider.focus();468- await popup.keyboard.press("ArrowRight");469- await popup.waitForTimeout(100);470- await popup.keyboard.press("ArrowLeft");471- await popup.waitForTimeout(100);472- await ctx.close();473- });474-475- test("KBG03 - Space toggles option switches", async () => {476- const { popup } = await setupVault(ctx);477- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();478- await popup.waitForTimeout(300);479- await popup.getByRole("button", { name: /password generator/i }).click();480- await popup.waitForTimeout(500);481- const toggleRow = popup.locator(".peach-pwgen-toggle-row").first();482- if (await toggleRow.isVisible({ timeout: 2000 }).catch(() => false)) {483- await toggleRow.focus();484- await popup.keyboard.press(" ");485- await popup.waitForTimeout(200);486- }487- await ctx.close();488- });489-490- test("KBG04 - Escape closes generator", async () => {491- const { popup } = await setupVault(ctx);492- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();493- await popup.waitForTimeout(300);494- await popup.getByRole("button", { name: /password generator/i }).click();495- await popup.waitForTimeout(500);496- await popup.keyboard.press("Escape");497- await popup.waitForTimeout(300);498- const output = popup.locator(".peach-pwgen-output");499- await output.isHidden().catch(() => true);500- await ctx.close();501- });502-503- test("KBG05 - Enter copies generated password", async () => {504- const { popup } = await setupVault(ctx);505- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();506- await popup.waitForTimeout(300);507- await popup.getByRole("button", { name: /password generator/i }).click();508- await popup.waitForTimeout(500);509- const copyBtn = popup.getByRole("button", { name: /copy/i }).first();510- if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {511- await copyBtn.focus();512- await popup.keyboard.press("Enter");513- await popup.waitForTimeout(300);514- }515- await ctx.close();516- });517-});518-519-test.describe("Lock Screen Keyboard", () => {520- let ctx: BrowserContext;521- test.afterEach(async () => { await ctx.close(); });522- test.beforeEach(async () => { ctx = await makeContext(); });523-524- test("KBL01 - Tab navigates lock screen", async () => {525- const { popup: _popup } = await setupVault(ctx);526- const newPage = await ctx.newPage();527- const extId = await waitForSW(ctx);528- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);529- await newPage.waitForTimeout(2000);530- await newPage.keyboard.press("Tab");531- await newPage.waitForTimeout(100);532- const focused = await newPage.evaluate(() => document.activeElement?.tagName);533- expect(focused).toBeTruthy();534- await ctx.close();535- });536-537- test("KBL02 - Ctrl+Enter submits unlock", async () => {538- await setupVault(ctx);539- const newPage = await ctx.newPage();540- const extId = await waitForSW(ctx);541- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);542- await newPage.waitForTimeout(2000);543- await newPage.locator("#master-password").fill("password123");544- await newPage.keyboard.press("Control+Enter");545- await newPage.waitForTimeout(2000);546- const entries = newPage.locator("[class*=entry]");547- await expect(entries.first()).toBeVisible({ timeout: 5000 });548- await ctx.close();549- });550-551- test("KBL03 - Password field accepts paste", async () => {552- await setupVault(ctx);553- const newPage = await ctx.newPage();554- const extId = await waitForSW(ctx);555- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);556- await newPage.waitForTimeout(2000);557- await newPage.locator("#master-password").fill("password123");558- await newPage.keyboard.press("Control+a");559- await newPage.keyboard.press("Control+c");560- await newPage.locator("#master-password").fill("");561- await newPage.locator("#master-password").focus();562- await newPage.keyboard.press("Control+v");563- await newPage.waitForTimeout(200);564- const value = await newPage.locator("#master-password").inputValue();565- expect(value).toBe("password123");566- await ctx.close();567- });568-569- test("KBL04 - Show password toggle via click", async () => {570- const { popup: _popup } = await setupVault(ctx);571- const newPage = await ctx.newPage();572- const extId = await waitForSW(ctx);573- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);574- await newPage.waitForTimeout(2000);575- const showBtn = newPage.locator("[aria-label*='show' i], [aria-label*='reveal' i]").first();576- if (await showBtn.isVisible({ timeout: 2000 }).catch(() => false)) {577- await showBtn.click();578- await newPage.waitForTimeout(200);579- const inputType = await newPage.locator("#master-password").getAttribute("type");580- expect(inputType).toBe("text");581- }582- await ctx.close();583- });584-585- test("KBL05 - Shift+Tab navigates backwards on lock", async () => {586- const { popup: _popup } = await setupVault(ctx);587- const newPage = await ctx.newPage();588- const extId = await waitForSW(ctx);589- await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);590- await newPage.waitForTimeout(2000);591- await newPage.keyboard.press("Shift+Tab");592- await newPage.waitForTimeout(100);593- const focused = await newPage.evaluate(() => document.activeElement?.tagName);594- expect(focused).toBeTruthy();595- await ctx.close();596- });597-});598-599-test.describe("Edit Entry Keyboard", () => {600- let ctx: BrowserContext;601- test.afterEach(async () => { await ctx.close(); });602- test.beforeEach(async () => { ctx = await makeContext(); });603-604- test("KBD01 - Tab navigates edit form fields", async () => {605- const { popup } = await setupVault(ctx);606- await popup.locator('input[placeholder="search vault..."]').click();607- await popup.getByRole("button", { name: /new entry/i }).click();608- await popup.locator(".peach-new-name-input").fill("EditKB");609- await popup.locator('input[placeholder="domain.com"]').fill("https://editkb.com");610- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");611- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");612- await popup.getByRole("button", { name: /create/i }).click();613- await popup.waitForTimeout(1500);614- await popup.locator(".peach-entry-name-button").first().click();615- await popup.waitForTimeout(500);616- await popup.locator("button[aria-label='Edit']").first().click();617- await popup.waitForTimeout(500);618- await popup.keyboard.press("Tab");619- await popup.waitForTimeout(100);620- const focused = await popup.evaluate(() => document.activeElement?.tagName);621- expect(focused).toBe("INPUT");622- await ctx.close();623- });624-625- test("KBD02 - Enter saves edit", async () => {626- const { popup } = await setupVault(ctx);627- await popup.locator('input[placeholder="search vault..."]').click();628- await popup.getByRole("button", { name: /new entry/i }).click();629- await popup.locator(".peach-new-name-input").fill("EditSaveKB");630- await popup.locator('input[placeholder="domain.com"]').fill("https://editsavekb.com");631- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");632- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");633- await popup.getByRole("button", { name: /create/i }).click();634- await popup.waitForTimeout(1500);635- await popup.locator(".peach-entry-name-button").first().click();636- await popup.waitForTimeout(500);637- await popup.locator("button[aria-label='Edit']").first().click();638- await popup.waitForTimeout(500);639- await popup.locator(".peach-new-name-input").fill("EditSaveKBChanged");640- await popup.keyboard.press("Enter");641- await popup.waitForTimeout(1500);642- await expect(popup.locator("text=EditSaveKBChanged")).toBeVisible({ timeout: 3000 });643- await ctx.close();644- });645-646- test("KBD03 - Escape cancels edit", async () => {647- const { popup } = await setupVault(ctx);648- await popup.locator('input[placeholder="search vault..."]').click();649- await popup.getByRole("button", { name: /new entry/i }).click();650- await popup.locator(".peach-new-name-input").fill("CancelEditKB");651- await popup.locator('input[placeholder="domain.com"]').fill("https://cancelkb.com");652- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");653- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");654- await popup.getByRole("button", { name: /create/i }).click();655- await popup.waitForTimeout(1500);656- await popup.locator(".peach-entry-name-button").first().click();657- await popup.waitForTimeout(500);658- await popup.locator("button[aria-label='Edit']").first().click();659- await popup.waitForTimeout(500);660- await popup.locator(".peach-new-name-input").fill("ShouldNotChange");661- await popup.keyboard.press("Escape");662- await popup.waitForTimeout(300);663- await expect(popup.locator("text=CancelEditKB")).toBeVisible({ timeout: 3000 });664- await ctx.close();665- });666-});667-668-test.describe("Search Field Keyboard", () => {669- let ctx: BrowserContext;670- test.afterEach(async () => { await ctx.close(); });671- test.beforeEach(async () => { ctx = await makeContext(); });672-673- test("KBS01 - Typing filters results immediately", async () => {674- const { popup } = await setupVault(ctx);675- for (const name of ["FilterTest1", "FilterTest2", "OtherEntry"]) {676- await popup.locator('input[placeholder="search vault..."]').click();677- await popup.getByRole("button", { name: /new entry/i }).click();678- await popup.locator(".peach-new-name-input").fill(name);679- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);680- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);681- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");682- await popup.getByRole("button", { name: /create/i }).click();683- await popup.waitForTimeout(800);684- }685- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();686- await searchInput.click();687- await searchInput.fill("Filter");688- await popup.waitForTimeout(300);689- await expect(popup.locator("text=FilterTest1")).toBeVisible();690- await expect(popup.locator("text=OtherEntry")).not.toBeVisible();691- await ctx.close();692- });693-694- test("KBS02 - Backspace reduces search query", async () => {695- const { popup } = await setupVault(ctx);696- await popup.locator('input[placeholder="search vault..."]').click();697- await popup.getByRole("button", { name: /new entry/i }).click();698- await popup.locator(".peach-new-name-input").fill("BackspaceTest");699- await popup.locator('input[placeholder="domain.com"]').fill("https://back.com");700- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");701- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");702- await popup.getByRole("button", { name: /create/i }).click();703- await popup.waitForTimeout(1500);704- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();705- await searchInput.click();706- await searchInput.fill("BackspaceTest");707- await popup.keyboard.press("Backspace");708- await popup.keyboard.press("Backspace");709- await popup.keyboard.press("Backspace");710- await popup.keyboard.press("Backspace");711- await popup.keyboard.press("Backspace");712- await popup.keyboard.press("Backspace");713- await popup.keyboard.press("Backspace");714- await popup.keyboard.press("Backspace");715- await popup.keyboard.press("Backspace");716- await popup.keyboard.press("Backspace");717- await popup.keyboard.press("Backspace");718- await popup.waitForTimeout(200);719- await ctx.close();720- });721-722- test("KBS03 - Clear search shows all entries", async () => {723- const { popup } = await setupVault(ctx);724- for (const name of ["ShowAll1", "ShowAll2"]) {725- await popup.locator('input[placeholder="search vault..."]').click();726- await popup.getByRole("button", { name: /new entry/i }).click();727- await popup.locator(".peach-new-name-input").fill(name);728- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);729- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);730- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");731- await popup.getByRole("button", { name: /create/i }).click();732- await popup.waitForTimeout(800);733- }734- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();735- await searchInput.click();736- await searchInput.fill("ShowAll1");737- await popup.waitForTimeout(300);738- await searchInput.fill("");739- await popup.waitForTimeout(300);740- await expect(popup.locator("text=ShowAll1")).toBeVisible();741- await expect(popup.locator("text=ShowAll2")).toBeVisible();742- await ctx.close();743- });744-745- test("KBS04 - Search with no results shows empty state", async () => {746- const { popup } = await setupVault(ctx);747- await popup.locator('input[placeholder="search vault..."]').click();748- await popup.getByRole("button", { name: /new entry/i }).click();749- await popup.locator(".peach-new-name-input").fill("SingleEntry");750- await popup.locator('input[placeholder="domain.com"]').fill("https://single.com");751- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");752- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");753- await popup.getByRole("button", { name: /create/i }).click();754- await popup.waitForTimeout(1500);755- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();756- await searchInput.click();757- await searchInput.fill("NothingMatches");758- await popup.waitForTimeout(500);759- const empty = popup.locator("text=no entries, text=no results, text=0 results");760- const hasEmpty = await empty.isVisible({ timeout: 2000 }).catch(() => false);761- expect(hasEmpty).toBeTruthy();762- await ctx.close();763- });764-765- test("KBS05 - Search preserves cursor position", async () => {766- const { popup } = await setupVault(ctx);767- await popup.locator('input[placeholder="search vault..."]').click();768- await popup.getByRole("button", { name: /new entry/i }).click();769- await popup.locator(".peach-new-name-input").fill("CursorPreserve");770- await popup.locator('input[placeholder="domain.com"]').fill("https://cursor.com");771- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");772- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");773- await popup.getByRole("button", { name: /create/i }).click();774- await popup.waitForTimeout(1500);775- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();776- await searchInput.click();777- await searchInput.fill("Cursor");778- await popup.keyboard.press("ArrowLeft");779- await popup.keyboard.press("ArrowLeft");780- await popup.waitForTimeout(100);781- await popup.keyboard.press("X");782- await popup.waitForTimeout(100);783- const value = await searchInput.inputValue();784- expect(value).toBe("CurXor");785- await ctx.close();786- });787-});\ No newline at end of filetests/keyboard-edge.spec.ts+0 −512
@@ -1,512 +0,0 @@1-import { test, expect, BrowserContext } from "@playwright/test";2-import { fileURLToPath } from "url";3-import { dirname, join } from "path";4-import os from "os";5-import fs from "fs/promises";6-7-const __filename = fileURLToPath(import.meta.url);8-const __dirname = dirname(__filename);9-const EXTENSION_PATH = join(__dirname, "..", "dist");10-11-async function makeContext() {12- const { chromium } = await import("playwright");13- const userDataDir = join(os.tmpdir(), `peach-kbd-${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-}20-21-async 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-}31-32-async 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-}46-47-test.describe("Keyboard Navigation", () => {48-49- test("KB01 - enter submits password on lock screen", async () => {50- const ctx = await makeContext();51- await setupVault(ctx);52- const newPopup = await ctx.newPage();53- const extId = await waitForSW(ctx);54- await newPopup.goto(`chrome-extension://${extId}/src/popup/index.html`);55- await newPopup.waitForTimeout(2000);56- await newPopup.locator("#master-password").fill("password123");57- await newPopup.keyboard.press("Enter");58- await newPopup.waitForTimeout(2000);59- const entries = newPopup.locator("[class*=entry]");60- await expect(entries.first()).toBeVisible({ timeout: 5000 });61- await ctx.close();62- });63-64- test("KB02 - tab navigates through fields", async () => {65- const ctx = await makeContext();66- const { popup } = await setupVault(ctx);67- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();68- await popup.waitForTimeout(500);69- await popup.keyboard.press("Tab");70- await popup.keyboard.press("Tab");71- const focused = await popup.evaluate(() => document.activeElement?.tagName);72- expect(focused).toBeTruthy();73- await ctx.close();74- });75-76- test("KB03 - escape closes settings panel", async () => {77- const ctx = await makeContext();78- const { popup } = await setupVault(ctx);79- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();80- await popup.waitForTimeout(500);81- await popup.keyboard.press("Escape");82- await popup.waitForTimeout(300);83- const overlay = popup.locator(".peach-overlay-view");84- const isHidden = await overlay.isHidden().catch(() => true);85- expect(isHidden).toBeTruthy();86- await ctx.close();87- });88-89- test("KB04 - arrow keys navigate entry list", async () => {90- const ctx = await makeContext();91- const { popup } = await setupVault(ctx);92- for (const name of ["Alpha", "Beta", "Gamma"]) {93- await popup.locator('input[placeholder="search vault..."]').click();94- await popup.getByRole("button", { name: /new entry/i }).click();95- await popup.locator(".peach-new-name-input").fill(name);96- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);97- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);98- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");99- await popup.getByRole("button", { name: /create/i }).click();100- await popup.waitForTimeout(500);101- }102- await popup.locator('input[placeholder="search vault..."]').click();103- await popup.keyboard.press("ArrowDown");104- await popup.waitForTimeout(200);105- await ctx.close();106- });107-108- test("KB05 - enter opens entry from list", async () => {109- const ctx = await makeContext();110- const { popup } = await setupVault(ctx);111- await popup.locator('input[placeholder="search vault..."]').click();112- await popup.getByRole("button", { name: /new entry/i }).click();113- await popup.locator(".peach-new-name-input").fill("EnterTest");114- await popup.locator('input[placeholder="domain.com"]').fill("https://entertest.com");115- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");116- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");117- await popup.getByRole("button", { name: /create/i }).click();118- await popup.waitForTimeout(1000);119- await popup.locator('input[placeholder="search vault..."]').click();120- await popup.waitForTimeout(300);121- await popup.keyboard.press("Enter");122- await popup.waitForTimeout(500);123- await ctx.close();124- });125-126- test("KB06 - ctrl+k focuses search", async () => {127- const ctx = await makeContext();128- const { popup } = await setupVault(ctx);129- await popup.keyboard.press("Control+k");130- await popup.waitForTimeout(200);131- const searchFocused = await popup.evaluate(() => {132- const el = document.activeElement;133- return el?.classList.contains("peach-search-input") || el?.closest(".peach-search-input");134- });135- expect(searchFocused).toBeTruthy();136- await ctx.close();137- });138-139- test("KB07 - backspace clears search", async () => {140- const ctx = await makeContext();141- const { popup } = await setupVault(ctx);142- await popup.locator('input[placeholder="search vault..."]').click();143- await popup.locator('input[placeholder="search vault..."]').fill("test");144- await popup.keyboard.press("Backspace");145- await popup.keyboard.press("Backspace");146- await popup.keyboard.press("Backspace");147- await popup.keyboard.press("Backspace");148- await ctx.close();149- });150-});151-152-test.describe("Edge Cases & Error Handling", () => {153-154- test("EDGE01 - entry with very long title", async () => {155- const ctx = await makeContext();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- const longTitle = "A".repeat(200);160- await popup.locator(".peach-new-name-input").fill(longTitle);161- await popup.locator('input[placeholder="domain.com"]').fill("https://longtitle.com");162- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");163- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");164- await popup.getByRole("button", { name: /create/i }).click();165- await popup.waitForTimeout(1500);166- await ctx.close();167- });168-169- test("EDGE02 - entry with unicode in all fields", async () => {170- const ctx = await makeContext();171- const { popup } = await setupVault(ctx);172- await popup.locator('input[placeholder="search vault..."]').click();173- await popup.getByRole("button", { name: /new entry/i }).click();174- await popup.locator(".peach-new-name-input").fill("日本語テスト");175- await popup.locator('input[placeholder="domain.com"]').fill("https://日本語.com");176- await popup.locator('input[placeholder="username or email"]').fill("用户@例子.测试");177- await popup.locator(".peach-new-field-input[placeholder='password']").fill("密码🔐");178- await popup.getByRole("button", { name: /create/i }).click();179- await popup.waitForTimeout(1500);180- await expect(popup.locator("text=日本語テスト")).toBeVisible({ timeout: 3000 });181- await ctx.close();182- });183-184- test("EDGE03 - very short password accepted", async () => {185- const ctx = await makeContext();186- const { popup } = await setupVault(ctx);187- await popup.locator('input[placeholder="search vault..."]').click();188- await popup.getByRole("button", { name: /new entry/i }).click();189- await popup.locator(".peach-new-name-input").fill("ShortPass");190- await popup.locator('input[placeholder="domain.com"]').fill("https://short.com");191- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");192- await popup.locator(".peach-new-field-input[placeholder='password']").fill("a");193- await popup.getByRole("button", { name: /create/i }).click();194- await popup.waitForTimeout(1500);195- await ctx.close();196- });197-198- test("EDGE04 - entry with special chars in domain", async () => {199- const ctx = await makeContext();200- const { popup } = await setupVault(ctx);201- await popup.locator('input[placeholder="search vault..."]').click();202- await popup.getByRole("button", { name: /new entry/i }).click();203- await popup.locator(".peach-new-name-input").fill("SpecialDomain");204- await popup.locator('input[placeholder="domain.com"]').fill("https://sub.domain.with-dashes.and_underscores.example.com/path?query=value");205- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");206- await popup.locator(".peach-new-field-input[placeholder='password']").fill("specialpass");207- await popup.getByRole("button", { name: /create/i }).click();208- await popup.waitForTimeout(1500);209- await ctx.close();210- });211-212- test("EDGE05 - entry with no username", async () => {213- const ctx = await makeContext();214- const { popup } = await setupVault(ctx);215- await popup.locator('input[placeholder="search vault..."]').click();216- await popup.getByRole("button", { name: /new entry/i }).click();217- await popup.locator(".peach-new-name-input").fill("NoUsername");218- await popup.locator('input[placeholder="domain.com"]').fill("https://nouser.com");219- await popup.locator('input[placeholder="username or email"]').fill("");220- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");221- await popup.getByRole("button", { name: /create/i }).click();222- await popup.waitForTimeout(1500);223- await ctx.close();224- });225-226- test("EDGE06 - entry with no domain", async () => {227- const ctx = await makeContext();228- const { popup } = await setupVault(ctx);229- await popup.locator('input[placeholder="search vault..."]').click();230- await popup.getByRole("button", { name: /new entry/i }).click();231- await popup.locator(".peach-new-name-input").fill("NoDomain");232- await popup.locator('input[placeholder="domain.com"]').fill("");233- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");234- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");235- await popup.getByRole("button", { name: /create/i }).click();236- await popup.waitForTimeout(1500);237- await ctx.close();238- });239-240- test("EDGE07 - rapid add entry button clicks", async () => {241- const ctx = await makeContext();242- const { popup } = await setupVault(ctx);243- const addBtn = popup.getByRole("button", { name: /new entry/i });244- await addBtn.click();245- await addBtn.click();246- await addBtn.click();247- await popup.waitForTimeout(500);248- const modal = popup.locator(".peach-new-body");249- await expect(modal).toBeVisible({ timeout: 3000 });250- await ctx.close();251- });252-253- test("EDGE08 - search with special regex chars", async () => {254- const ctx = await makeContext();255- const { popup } = await setupVault(ctx);256- for (const name of ["Test[0]", "Test(1)", "Test{2}"]) {257- await popup.locator('input[placeholder="search vault..."]').click();258- await popup.getByRole("button", { name: /new entry/i }).click();259- await popup.locator(".peach-new-name-input").fill(name);260- await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase().replace(/[^a-z]/g, "")}.com`);261- await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);262- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");263- await popup.getByRole("button", { name: /create/i }).click();264- await popup.waitForTimeout(500);265- }266- const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();267- await searchInput.click();268- await searchInput.fill("[0]");269- await popup.waitForTimeout(300);270- await ctx.close();271- });272-273- test("EDGE09 - copy button during loading state", async () => {274- const ctx = await makeContext();275- const { popup } = await setupVault(ctx);276- await popup.locator('input[placeholder="search vault..."]').click();277- await popup.getByRole("button", { name: /new entry/i }).click();278- await popup.locator(".peach-new-name-input").fill("CopyLoading");279- await popup.locator('input[placeholder="domain.com"]').fill("https://copyloading.com");280- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");281- await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");282- await popup.getByRole("button", { name: /create/i }).click();283- await popup.waitForTimeout(1500);284- const copyBtn = popup.locator(".peach-copy-btn").first();285- if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {286- await copyBtn.click();287- await popup.waitForTimeout(500);288- }289- await ctx.close();290- });291-292- test("EDGE10 - unlock with whitespace password", async () => {293- const ctx = await makeContext();294- const extId = await waitForSW(ctx);295- const popup = await ctx.newPage();296- await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);297- await popup.waitForTimeout(2000);298- await popup.locator("#master-password").fill(" ");299- await popup.getByRole("button", { name: /unlock/i }).click();300- await popup.waitForTimeout(500);301- const errorMsg = popup.locator(".peach-hint-error, .peach-hint");302- await errorMsg.first().isVisible({ timeout: 2000 }).catch(() => false);303- await ctx.close();304- });305-});306-307-test.describe("Import Export Round-trip", () => {308-309- test("IMP01 - import CSV and verify entries", async () => {310- const ctx = await makeContext();311- const { popup } = await setupVault(ctx);312- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();313- await popup.waitForTimeout(300);314- await popup.getByRole("button", { name: /import/i }).click();315- await popup.waitForTimeout(500);316- const fileInput = popup.locator('input[type="file"]');317- const csvContent = "title,username,password,url\nTestImport,importuser,importpass,https://import.com";318- await fileInput.setInputFiles({319- name: "test.csv",320- mimeType: "text/csv",321- buffer: Buffer.from(csvContent)322- });323- await popup.waitForTimeout(1000);324- await ctx.close();325- });326-327- test("IMP02 - export creates valid JSON file", async () => {328- const ctx = await makeContext();329- const { popup } = await setupVault(ctx);330- await popup.locator('input[placeholder="search vault..."]').click();331- await popup.getByRole("button", { name: /new entry/i }).click();332- await popup.locator(".peach-new-name-input").fill("ExportTest");333- await popup.locator('input[placeholder="domain.com"]').fill("https://exporttest.com");334- await popup.locator('input[placeholder="username or email"]').fill("[email protected]");335- await popup.locator(".peach-new-field-input[placeholder='password']").fill("exportpass");336- await popup.getByRole("button", { name: /create/i }).click();337- await popup.waitForTimeout(1500);338- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();339- await popup.waitForTimeout(300);340- await popup.getByRole("button", { name: /export/i }).click();341- await popup.waitForTimeout(500);342- await ctx.close();343- });344-345- test("IMP03 - empty vault export works", async () => {346- const ctx = await makeContext();347- const { popup } = await setupVault(ctx);348- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();349- await popup.waitForTimeout(300);350- await popup.getByRole("button", { name: /export/i }).click();351- await popup.waitForTimeout(500);352- await ctx.close();353- });354-355- test("IMP04 - import 1Password JSON format", async () => {356- const ctx = await makeContext();357- const { popup } = await setupVault(ctx);358- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();359- await popup.waitForTimeout(300);360- await popup.getByRole("button", { name: /import/i }).click();361- await popup.waitForTimeout(500);362- const fileInput = popup.locator('input[type="file"]');363- const json1pContent = JSON.stringify({364- accounts: [{365- vaults: [{366- items: [{367- overview: { title: "1P Test", url: "https://1p.com" },368- details: {369- loginFields: [370- { designation: "username", value: "testuser" },371- { designation: "password", value: "testpass" }372- ]373- }374- }]375- }]376- }]377- });378- await fileInput.setInputFiles({379- name: "1p-export.json",380- mimeType: "application/json",381- buffer: Buffer.from(json1pContent)382- });383- await popup.waitForTimeout(1000);384- await ctx.close();385- });386-});387-388-test.describe("Password Generator", () => {389-390- test("GEN01 - generates passwords of exact length", async () => {391- const ctx = await makeContext();392- const { popup } = await setupVault(ctx);393- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();394- await popup.waitForTimeout(300);395- await popup.getByRole("button", { name: /password generator/i }).click();396- await popup.waitForTimeout(500);397- const slider = popup.locator(".peach-pwgen-slider");398- await slider.fill("32");399- await popup.waitForTimeout(300);400- const output = await popup.locator(".peach-pwgen-output").textContent();401- expect(output?.length).toBe(32);402- await ctx.close();403- });404-405- test("GEN02 - regenerate produces new password", async () => {406- const ctx = await makeContext();407- const { popup } = await setupVault(ctx);408- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();409- await popup.waitForTimeout(300);410- await popup.getByRole("button", { name: /password generator/i }).click();411- await popup.waitForTimeout(500);412- const pwd1 = await popup.locator(".peach-pwgen-output").textContent();413- await popup.locator("button[aria-label='Regenerate']").click();414- await popup.waitForTimeout(300);415- const pwd2 = await popup.locator(".peach-pwgen-output").textContent();416- expect(pwd1).not.toEqual(pwd2);417- await ctx.close();418- });419-420- test("GEN03 - toggle uppercase on/off", async () => {421- const ctx = await makeContext();422- const { popup } = await setupVault(ctx);423- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();424- await popup.waitForTimeout(300);425- await popup.getByRole("button", { name: /password generator/i }).click();426- await popup.waitForTimeout(500);427- const toggles = popup.locator(".peach-pwgen-toggle-row");428- const count = await toggles.count();429- if (count >= 2) {430- await toggles.nth(0).click();431- await popup.waitForTimeout(200);432- }433- await ctx.close();434- });435-436- test("GEN04 - toggle numbers on/off", async () => {437- const ctx = await makeContext();438- const { popup } = await setupVault(ctx);439- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();440- await popup.waitForTimeout(300);441- await popup.getByRole("button", { name: /password generator/i }).click();442- await popup.waitForTimeout(500);443- const toggles = popup.locator(".peach-pwgen-toggle-row");444- const count = await toggles.count();445- if (count >= 3) {446- await toggles.nth(2).click();447- await popup.waitForTimeout(200);448- }449- await ctx.close();450- });451-452- test("GEN05 - toggle symbols on/off", async () => {453- const ctx = await makeContext();454- const { popup } = await setupVault(ctx);455- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();456- await popup.waitForTimeout(300);457- await popup.getByRole("button", { name: /password generator/i }).click();458- await popup.waitForTimeout(500);459- const toggles = popup.locator(".peach-pwgen-toggle-row");460- const count = await toggles.count();461- if (count >= 4) {462- await toggles.nth(3).click();463- await popup.waitForTimeout(200);464- }465- await ctx.close();466- });467-468- test("GEN06 - min length 8 enforced", async () => {469- const ctx = await makeContext();470- const { popup } = await setupVault(ctx);471- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();472- await popup.waitForTimeout(300);473- await popup.getByRole("button", { name: /password generator/i }).click();474- await popup.waitForTimeout(500);475- const slider = popup.locator(".peach-pwgen-slider");476- await slider.fill("4");477- await popup.waitForTimeout(300);478- const output = await popup.locator(".peach-pwgen-output").textContent();479- expect(output?.length).toBeGreaterThanOrEqual(8);480- await ctx.close();481- });482-483- test("GEN07 - max length 64 enforced", async () => {484- const ctx = await makeContext();485- const { popup } = await setupVault(ctx);486- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();487- await popup.waitForTimeout(300);488- await popup.getByRole("button", { name: /password generator/i }).click();489- await popup.waitForTimeout(500);490- const slider = popup.locator(".peach-pwgen-slider");491- await slider.fill("128");492- await popup.waitForTimeout(300);493- const output = await popup.locator(".peach-pwgen-output").textContent();494- expect(output?.length).toBeLessThanOrEqual(64);495- await ctx.close();496- });497-498- test("GEN08 - use password fills into entry", async () => {499- const ctx = await makeContext();500- const { popup } = await setupVault(ctx);501- await popup.locator(".peach-icon-btn[aria-label='Settings']").click();502- await popup.waitForTimeout(300);503- await popup.getByRole("button", { name: /password generator/i }).click();504- await popup.waitForTimeout(500);505- const useBtn = popup.getByRole("button", { name: /use/i });506- if (await useBtn.isVisible({ timeout: 2000 }).catch(() => false)) {507- await useBtn.click();508- await popup.waitForTimeout(500);509- }510- await ctx.close();511- });512-});\ No newline at end of filetests/passkeys.spec.ts+1 −2
@@ -1,4 +1,4 @@1-import { test, expect, type BrowserContext, type Page } from "@playwright/test";1+import { chromium, test, expect, type BrowserContext, type Page } from "@playwright/test";22 import { createServer, type Server } from "http";33 import { readFile } from "fs/promises";44 import { fileURLToPath } from "url";@@ -34,7 +34,6 @@3434 }3535 3636 async function makeContext() {37- const { chromium } = await import("playwright");3837 const userDataDir = join(3938 os.tmpdir(),4039 `peach-passkeys-${Date.now()}-${Math.random().toString(36).slice(2)}`tests/password-generator.spec.ts+0 −464
not shown here: too long for one page
tests/settings-persistence.spec.ts+0 −637
not shown here: too long for one page
tests/vault-security.spec.ts+2 −3
@@ -1,4 +1,4 @@1-import { test, expect, BrowserContext } from "@playwright/test";1+import { chromium, test, expect, BrowserContext } from "@playwright/test";22 import { fileURLToPath } from "url";33 import { dirname, join } from "path";44 import os from "os";@@ -116,7 +116,6 @@116116 });117117 118118 test("SECS02 - session does not persist across browser restarts", async () => {119- const { chromium } = await import("playwright");120119 const userDataDir = join(os.tmpdir(), `peach-sec-restart-${Date.now()}`);121120 await fs.mkdir(userDataDir, { recursive: true });122121 const launch = () => chromium.launchPersistentContext(userDataDir, {@@ -428,4 +427,4 @@428427 expect(uniqueChars).toBeGreaterThan(10);429428 await ctx.close();430429 });431-});\ No newline at end of file430+});tests/visual-compliance.spec.ts+0 −624
not shown here: too long for one page