1import { test, expect, 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 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}2021async 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}3132async 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}4647test.describe("Accessibility", () => {4849 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 });6768 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 });8889 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 });103104 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 });113114 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 });124125 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 });144145 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 });153154 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});165166test.describe("TOTP Functionality", () => {167168 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 });183184 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 });203204 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 });225226 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 });248249 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 });268269 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});289290test.describe("Vault Management", () => {291292 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 });306307 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 });314315 test("VAULT03 - biometric unlock shows when available", async () => {316 const ctx = await makeContext();317 const extId = await waitForSW(ctx);318 const popup = await ctx.newPage();319 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);320 await popup.waitForTimeout(2000);321 const biometric = popup.locator("text=biometric, text=fingerprint, text=face id");322 await biometric.first().waitFor({ state: "visible", timeout: 3000 }).catch(() => {});323 await ctx.close();324 });325326 test("VAULT04 - vault persists through navigation", async () => {327 const ctx = await makeContext();328 const { popup } = await setupVault(ctx);329 await popup.locator('input[placeholder="search vault..."]').click();330 await popup.getByRole("button", { name: /new entry/i }).click();331 await popup.locator(".peach-new-name-input").fill("PersistNav");332 await popup.locator('input[placeholder="domain.com"]').fill("https://persistnav.com");333 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");334 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");335 await pwdField.fill("pass123");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: /password generator/i }).click();341 await popup.waitForTimeout(500);342 await popup.keyboard.press("Escape");343 await popup.waitForTimeout(300);344 const entry = popup.locator("text=PersistNav");345 await expect(entry).toBeVisible({ timeout: 3000 });346 await ctx.close();347 });348349 test("VAULT05 - re-lock and unlock vault", async () => {350 const ctx = await makeContext();351 const { popup } = await setupVault(ctx);352 const lockBtn = popup.locator(".peach-lock-btn, [aria-label='Lock vault']").first();353 if (await lockBtn.isVisible({ timeout: 2000 }).catch(() => false)) {354 await lockBtn.click();355 await popup.waitForTimeout(1000);356 await popup.locator("#master-password").fill("password123");357 await popup.getByRole("button", { name: /unlock/i }).click();358 await popup.waitForTimeout(2000);359 const entries = popup.locator("[class*=entry]");360 await expect(entries.first()).toBeVisible({ timeout: 5000 });361 }362 await ctx.close();363 });364365 test("VAULT06 - search works with Cyrillic text", async () => {366 const ctx = await makeContext();367 const { popup } = await setupVault(ctx);368 await popup.locator('input[placeholder="search vault..."]').click();369 await popup.getByRole("button", { name: /new entry/i }).click();370 await popup.locator(".peach-new-name-input").fill("ТестПароль");371 await popup.locator('input[placeholder="domain.com"]').fill("https://тест.com");372 await popup.locator('input[placeholder="username or email"]').fill("пользователь@тест.com");373 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");374 await pwdField.fill("пароль123");375 await popup.getByRole("button", { name: /create/i }).click();376 await popup.waitForTimeout(1500);377 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();378 await searchInput.click();379 await searchInput.fill("Тест");380 await popup.waitForTimeout(300);381 await expect(popup.locator("text=ТестПароль")).toBeVisible({ timeout: 3000 });382 await ctx.close();383 });384385 test("VAULT07 - entries can be sorted", async () => {386 const ctx = await makeContext();387 const { popup } = await setupVault(ctx);388 for (const name of ["Zebra", "Apple", "Mango"]) {389 await popup.locator('input[placeholder="search vault..."]').click();390 await popup.getByRole("button", { name: /new entry/i }).click();391 await popup.locator(".peach-new-name-input").fill(name);392 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);393 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`);394 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");395 await pwdField.fill("pass123");396 await popup.getByRole("button", { name: /create/i }).click();397 await popup.waitForTimeout(500);398 }399 await ctx.close();400 });401402 test("VAULT08 - delete entry removes it from list", async () => {403 const ctx = await makeContext();404 const { popup } = await setupVault(ctx);405 await popup.locator('input[placeholder="search vault..."]').click();406 await popup.getByRole("button", { name: /new entry/i }).click();407 await popup.locator(".peach-new-name-input").fill("DeleteMe");408 await popup.locator('input[placeholder="domain.com"]').fill("https://deleteme.com");409 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");410 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");411 await pwdField.fill("pass123");412 await popup.getByRole("button", { name: /create/i }).click();413 await popup.waitForTimeout(1500);414 await popup.locator("text=DeleteMe").first().click();415 await popup.waitForTimeout(500);416 const deleteLink = popup.locator("text=delete entry");417 if (await deleteLink.isVisible({ timeout: 2000 }).catch(() => false)) {418 await deleteLink.click();419 await popup.waitForTimeout(1000);420 }421 await ctx.close();422 });423});