import { test, expect, 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 makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-a11y-${Date.now()}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } 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 setupVault(ctx: BrowserContext): Promise<{ popup: any, 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); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill("password123"); await popup.locator("#confirm-password").fill("password123"); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup, extId }; } test.describe("Accessibility", () => { test("A11Y01 - all buttons have accessible names", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const buttons = popup.locator("button"); const count = await buttons.count(); let allAccessible = true; for (let i = 0; i < Math.min(count, 20); i++) { const btn = buttons.nth(i); const ariaLabel = await btn.getAttribute("aria-label"); const text = await btn.textContent().catch(() => ""); if (!ariaLabel && !text?.trim()) { allAccessible = false; break; } } expect(allAccessible).toBe(true); await ctx.close(); }); test("A11Y02 - form inputs have associated labels", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const inputs = popup.locator("input:not([type='hidden']):not([type='checkbox'])"); const count = await inputs.count(); let allLabeled = true; for (let i = 0; i < Math.min(count, 10); i++) { const input = inputs.nth(i); const id = await input.getAttribute("id"); const ariaLabel = await input.getAttribute("aria-label"); const ariaLabelledby = await input.getAttribute("aria-labelledby"); const placeholder = await input.getAttribute("placeholder"); if (!id && !ariaLabel && !ariaLabelledby && !placeholder) { allLabeled = false; break; } } expect(allLabeled).toBe(true); await ctx.close(); }); test("A11Y03 - color contrast for text elements", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const textColor = await popup.evaluate(() => { const el = document.querySelector(".peach-entry-name, .peach-label, body"); return window.getComputedStyle(el!).color; }); const bgColor = await popup.evaluate(() => { return window.getComputedStyle(document.body).backgroundColor; }); expect(textColor).toBeTruthy(); expect(bgColor).toBeTruthy(); await ctx.close(); }); test("A11Y04 - focus indicators visible on interactive elements", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.keyboard.press("Tab"); const focusedEl = await popup.evaluate(() => document.activeElement?.tagName); expect(focusedEl).toBeTruthy(); await ctx.close(); }); test("A11Y05 - ARIA roles used correctly", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const switchRole = popup.locator("[role='switch']"); const optionRole = popup.locator("[role='option']"); const hasSwitch = await switchRole.count(); const hasOption = await optionRole.count(); expect(hasSwitch + hasOption).toBeGreaterThan(0); await ctx.close(); }); test("A11Y06 - screen reader announces copy feedback", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("A11YTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://a11y.com"); await popup.locator('input[placeholder="username or email"]').fill("user@a11y.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const copyBtn = popup.locator(".peach-copy-btn").first(); if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await copyBtn.click(); await popup.waitForTimeout(300); } await ctx.close(); }); test("A11Y07 - landmark regions identified", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const mainLandmark = popup.locator("main, [role='main']"); const hasMain = await mainLandmark.count(); expect(hasMain).toBeGreaterThan(0); await ctx.close(); }); test("A11Y08 - lang attribute on html element", async () => { const ctx = await makeContext(); const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(2000); const lang = await popup.getAttribute("html", "lang"); expect(lang).toBeTruthy(); await ctx.close(); }); }); test.describe("TOTP Functionality", () => { test("TOTP01 - TOTP field accepts base32 secret", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("TOTPBase32"); await popup.locator('input[placeholder="domain.com"]').fill("https://totp.com"); await popup.locator('input[placeholder="username or email"]').fill("user@totp.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("TOTP02 - TOTP code displays in entry view", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("TOTPDisplay"); await popup.locator('input[placeholder="domain.com"]').fill("https://display.com"); await popup.locator('input[placeholder="username or email"]').fill("user@display.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(2000); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(1000); const totpCode = popup.locator(".peach-totp-code"); await expect(totpCode).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("TOTP03 - TOTP countdown timer shows remaining seconds", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("TOTPTimer"); await popup.locator('input[placeholder="domain.com"]').fill("https://timer.com"); await popup.locator('input[placeholder="username or email"]').fill("user@timer.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=TOTPTimer").first().click(); await popup.waitForTimeout(500); const countdown = popup.locator(".peach-totp-countdown"); await expect(countdown).toBeVisible({ timeout: 3000 }); const remaining = await countdown.textContent(); expect(parseInt(remaining ?? "0")).toBeGreaterThan(0); await ctx.close(); }); test("TOTP04 - copy TOTP button works", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("TOTPCopy"); await popup.locator('input[placeholder="domain.com"]').fill("https://copytotp.com"); await popup.locator('input[placeholder="username or email"]').fill("user@copytotp.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=TOTPCopy").first().click(); await popup.waitForTimeout(500); const totpCopyBtn = popup.locator("[aria-label='Copy TOTP code']"); if (await totpCopyBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await totpCopyBtn.click(); await popup.waitForTimeout(300); } await ctx.close(); }); test("TOTP05 - TOTP ring progress animates", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("TOTPRing"); await popup.locator('input[placeholder="domain.com"]').fill("https://ring.com"); await popup.locator('input[placeholder="username or email"]').fill("user@ring.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=TOTPRing").first().click(); await popup.waitForTimeout(500); const ring = popup.locator(".peach-totp-ring"); await expect(ring).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("TOTP06 - entry without TOTP shows no totp UI", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("NoTOTP"); await popup.locator('input[placeholder="domain.com"]').fill("https://nototp.com"); await popup.locator('input[placeholder="username or email"]').fill("user@nototp.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=NoTOTP").first().click(); await popup.waitForTimeout(500); const totpBand = popup.locator(".peach-totp-band"); const isHidden = await totpBand.isHidden().catch(() => true); expect(isHidden).toBe(true); await ctx.close(); }); }); test.describe("Vault Management", () => { test("VAULT01 - create vault with different passwords shows mismatch error", async () => { const ctx = await makeContext(); const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(2000); await popup.locator("#master-password").fill("password123"); await popup.locator("#confirm-password").fill("differentpassword"); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(500); const error = popup.locator(".peach-hint-error"); await expect(error).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("VAULT02 - clear vault option exists in settings", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await ctx.close(); }); test("VAULT03 - biometric unlock shows when available", async () => { const ctx = await makeContext(); const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(2000); const biometric = popup.locator("text=biometric, text=fingerprint, text=face id"); await biometric.first().waitFor({ state: "visible", timeout: 3000 }).catch(() => {}); await ctx.close(); }); test("VAULT04 - vault persists through navigation", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("PersistNav"); await popup.locator('input[placeholder="domain.com"]').fill("https://persistnav.com"); await popup.locator('input[placeholder="username or email"]').fill("user@persistnav.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.getByRole("button", { name: /password generator/i }).click(); await popup.waitForTimeout(500); await popup.keyboard.press("Escape"); await popup.waitForTimeout(300); const entry = popup.locator("text=PersistNav"); await expect(entry).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("VAULT05 - re-lock and unlock vault", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const lockBtn = popup.locator(".peach-lock-btn, [aria-label='Lock vault']").first(); if (await lockBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await lockBtn.click(); await popup.waitForTimeout(1000); await popup.locator("#master-password").fill("password123"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(2000); const entries = popup.locator("[class*=entry]"); await expect(entries.first()).toBeVisible({ timeout: 5000 }); } await ctx.close(); }); test("VAULT06 - search works with Cyrillic text", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("ТестПароль"); await popup.locator('input[placeholder="domain.com"]').fill("https://тест.com"); await popup.locator('input[placeholder="username or email"]').fill("пользователь@тест.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("пароль123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first(); await searchInput.click(); await searchInput.fill("Тест"); await popup.waitForTimeout(300); await expect(popup.locator("text=ТестПароль")).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("VAULT07 - entries can be sorted", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); for (const name of ["Zebra", "Apple", "Mango"]) { await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); 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`); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(500); } await ctx.close(); }); test("VAULT08 - delete entry removes it from list", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("DeleteMe"); await popup.locator('input[placeholder="domain.com"]').fill("https://deleteme.com"); await popup.locator('input[placeholder="username or email"]').fill("del@ete.me"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=DeleteMe").first().click(); await popup.waitForTimeout(500); const deleteLink = popup.locator("text=delete entry"); if (await deleteLink.isVisible({ timeout: 2000 }).catch(() => false)) { await deleteLink.click(); await popup.waitForTimeout(1000); } await ctx.close(); }); });