1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");1011async function 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}2122async 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}2930async 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}3940async 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}5152async 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}6364test.describe("Clipboard - Copy Password", () => {65 let ctx: BrowserContext;66 test.afterEach(async () => { await ctx.close(); });67 test.beforeEach(async () => { ctx = await makeContext(); });6869 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 });7879 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 });8990 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 });103104 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 });117118 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 });131132 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 });141142 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 });152153 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 });163164 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 });182183 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});194195test.describe("Clipboard - Copy Username", () => {196 let ctx: BrowserContext;197 test.afterEach(async () => { await ctx.close(); });198 test.beforeEach(async () => { ctx = await makeContext(); });199200 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 });210211 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 });224225 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 });238239 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});250251test.describe("Clipboard - Settings", () => {252 let ctx: BrowserContext;253 test.afterEach(async () => { await ctx.close(); });254 test.beforeEach(async () => { ctx = await makeContext(); });255256 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 });265266 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 });277278 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 });289290 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 });301302 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 });313314 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});335336test.describe("Clipboard - Security", () => {337 let ctx: BrowserContext;338 test.afterEach(async () => { await ctx.close(); });339 test.beforeEach(async () => { ctx = await makeContext(); });340341 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 });351352 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 });364365 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});