import { test, expect, Page, BrowserContext } from "@playwright/test"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import os from "os"; import fs from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const EXTENSION_PATH = join(__dirname, "..", "dist"); async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise(r => setTimeout(r, 500)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No SW after ${maxWait}ms`); return new URL(sw.url()).host; } async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> { const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(3000); return { popup, extId }; } async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } async function setupVault(ctx: BrowserContext, password = "password123") { const { popup } = await loadPopup(ctx); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill(password); await popup.locator("#confirm-password").fill(password); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup }; } async function createEntry(popup: Page, name: string) { await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.waitForTimeout(300); await popup.locator(".peach-new-name-input").fill(name); await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`); await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`); await popup.locator(".peach-new-field-input[placeholder='password']").fill("testpass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(800); } test.describe("Clipboard - Copy Password", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("CP01 - copy button is visible in entry detail", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await expect(copyBtn).toBeVisible(); await ctx.close(); }); test("CP02 - copy button has correct aria-label", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); const ariaLabel = await copyBtn.getAttribute("aria-label"); expect(ariaLabel).toContain("copy"); await ctx.close(); }); test("CP03 - copy button copies password to clipboard", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(300); const copied = popup.locator("text=/copied|\\u2713/i"); const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false); expect(hasCopied).toBeTruthy(); await ctx.close(); }); test("CP04 - copy shows temporary notification", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(500); const notification = popup.locator("text=/copied to clipboard|✓ copied/i"); const hasNotification = await notification.isVisible().catch(() => false); expect(hasNotification).toBeTruthy(); await ctx.close(); }); test("CP05 - copied notification disappears after timeout", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(3000); const notification = popup.locator("text=/copied/i"); const isGone = await notification.isHidden().catch(() => false); expect(isGone).toBeTruthy(); await ctx.close(); }); test("CP06 - copy password works with keyboard shortcut", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); await popup.keyboard.press("Control+Shift+C"); await popup.waitForTimeout(500); await ctx.close(); }); test("CP07 - copy button is disabled during loading", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(200); await ctx.close(); }); test("CP08 - copy clears clipboard after timeout", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(45000); await ctx.close(); }); test("CP09 - multiple copy operations work", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Entry1"); await createEntry(popup, "Entry2"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(500); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const newCopyBtn = popup.locator("[aria-label*='copy' i]").first(); await newCopyBtn.click(); await popup.waitForTimeout(300); await ctx.close(); }); test("CP10 - copy button has tooltip", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "CopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.hover(); await popup.waitForTimeout(500); await ctx.close(); }); }); test.describe("Clipboard - Copy Username", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("CU01 - username copy button is visible", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "UserCopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtns = popup.locator("[aria-label*='copy' i]"); const count = await copyBtns.count(); expect(count).toBeGreaterThan(1); await ctx.close(); }); test("CU02 - copy username copies correct value", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "UserCopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtns = popup.locator("[aria-label*='copy' i]"); await copyBtns.nth(1).click(); await popup.waitForTimeout(300); const copied = popup.locator("text=/copied|\\u2713/i"); const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false); expect(hasCopied).toBeTruthy(); await ctx.close(); }); test("CU03 - username copy shows notification", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "UserCopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtns = popup.locator("[aria-label*='copy' i]"); await copyBtns.nth(1).click(); await popup.waitForTimeout(500); const notification = popup.locator("text=/copied/i"); const hasNotification = await notification.isVisible().catch(() => false); expect(hasNotification).toBeTruthy(); await ctx.close(); }); test("CU04 - copy username respects clipboard timeout", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "UserCopyTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtns = popup.locator("[aria-label*='copy' i]"); await copyBtns.nth(1).click(); await popup.waitForTimeout(300); await ctx.close(); }); }); test.describe("Clipboard - Settings", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("CS01 - clipboard clear setting is available", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); const clipboardSetting = popup.locator("text=/clipboard|clear/i"); const hasSetting = await clipboardSetting.isVisible().catch(() => false); expect(hasSetting).toBeTruthy(); await ctx.close(); }); test("CS02 - clipboard clear time options", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=/clipboard|clear/i").click(); await popup.waitForTimeout(300); const options = popup.locator("[role='option' i], [role='menuitem' i]"); const count = await options.count(); expect(count).toBeGreaterThan(1); await ctx.close(); }); test("CS03 - clipboard clear 30 seconds option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=/clipboard|clear/i").click(); await popup.waitForTimeout(300); const thirtySec = popup.locator("text=/30 seconds|30s/i"); const hasThirty = await thirtySec.isVisible().catch(() => false); expect(hasThirty).toBeTruthy(); await ctx.close(); }); test("CS04 - clipboard clear 1 minute option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=/clipboard|clear/i").click(); await popup.waitForTimeout(300); const oneMin = popup.locator("text=/1 minute|60 seconds/i"); const hasOneMin = await oneMin.isVisible().catch(() => false); expect(hasOneMin).toBeTruthy(); await ctx.close(); }); test("CS05 - clipboard clear never option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=/clipboard|clear/i").click(); await popup.waitForTimeout(300); const neverOpt = popup.locator("text=/never|disable/i"); const hasNever = await neverOpt.isVisible().catch(() => false); expect(hasNever).toBeTruthy(); await ctx.close(); }); test("CS06 - clipboard setting persists after lock", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=/clipboard|clear/i").click(); await popup.waitForTimeout(300); const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1); await option.click(); await popup.waitForTimeout(300); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click(); await popup.waitForTimeout(500); await popup.locator("#master-password").fill("password123"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(2000); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await ctx.close(); }); }); test.describe("Clipboard - Security", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("CSEC01 - sensitive data not logged", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "SecurityTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(300); await ctx.close(); }); test("CSEC02 - clipboard cleared on lock", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "SecurityTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(500); await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click(); await popup.waitForTimeout(500); await ctx.close(); }); test("CSEC03 - no clipboard data in page source", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "SecurityTest"); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); const copyBtn = popup.locator("[aria-label*='copy' i]").first(); await copyBtn.click(); await popup.waitForTimeout(300); const html = await popup.content(); expect(html).not.toContain("testpass123"); await ctx.close(); }); });