clipboard.spec.ts

15.1 KB · raw

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});