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-vc-${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("Animation & Transition Compliance", () => {48 let ctx: BrowserContext;49 test.afterEach(async () => { await ctx.close(); });50 test.beforeEach(async () => { ctx = await makeContext(); });5152 test("VAP01 - lock screen animations use only opacity and transform", async () => {53 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>54 f.filter(f => f.endsWith(".css"))55 );56 const nonAnimatableProps: string[] = [];57 for (const file of cssFiles) {58 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");59 const keyframes = content.match(/@keyframes[\s\S]*?\{[\s\S]*?\}/g) || [];60 for (const kf of keyframes) {61 const props = kf.match(/[\w-]+:\s*[^;]+;/g) || [];62 for (const prop of props) {63 const name = prop.split(":")[0].trim();64 if (!["opacity", "transform", "color", "background-color", "border-color", "stroke", "fill"].includes(name)) {65 nonAnimatableProps.push(`${file}: ${name} in @keyframes`);66 }67 }68 }69 }70 expect(nonAnimatableProps).toHaveLength(0);71 await ctx.close();72 });7374 test("VAP02 - spring easing variable used for animations", async () => {75 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>76 f.filter(f => f.endsWith(".css"))77 );78 const bezierWithoutVar: string[] = [];79 for (const file of cssFiles) {80 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");81 const beziers = content.match(/cubic-bezier\([^)]+\)/g) || [];82 for (const b of beziers) {83 if (!content.includes(`--spring`) && !content.includes(`--retreat`) && !content.includes(`--settle`)) {84 bezierWithoutVar.push(`${file}: ${b}`);85 }86 }87 }88 expect(bezierWithoutVar).toHaveLength(0);89 await ctx.close();90 });9192 test("VAP03 - no animation on 'display' property", async () => {93 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>94 f.filter(f => f.endsWith(".css"))95 );96 const displayAnimations: string[] = [];97 for (const file of cssFiles) {98 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");99 const keyframes = content.match(/@keyframes[\s\S]*?\{[\s\S]*?\}/g) || [];100 for (const kf of keyframes) {101 if (kf.includes("display")) {102 displayAnimations.push(file);103 }104 }105 }106 expect(displayAnimations).toHaveLength(0);107 await ctx.close();108 });109110 test("VAP04 - unlock transition is smooth (no jank)", async () => {111 await setupVault(ctx);112 const newPage = await ctx.newPage();113 const extId = await waitForSW(ctx);114 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);115 await newPage.waitForTimeout(2000);116 const start = Date.now();117 await newPage.locator("#master-password").fill("password123");118 await newPage.getByRole("button", { name: /unlock/i }).click();119 await newPage.waitForTimeout(100);120 const entries = newPage.locator("[class*=entry]");121 await entries.first().waitFor({ state: "visible", timeout: 5000 });122 const elapsed = Date.now() - start;123 expect(elapsed).toBeLessThan(3000);124 await newPage.close();125 await ctx.close();126 });127128 test("VAP05 - entry detail open animation is under 300ms", async () => {129 const { popup } = await setupVault(ctx);130 await popup.locator('input[placeholder="search vault..."]').click();131 await popup.getByRole("button", { name: /new entry/i }).click();132 await popup.locator(".peach-new-name-input").fill("AnimTest");133 await popup.locator('input[placeholder="domain.com"]').fill("https://anim.com");134 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");135 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");136 await popup.getByRole("button", { name: /create/i }).click();137 await popup.waitForTimeout(1500);138 const start = Date.now();139 await popup.locator(".peach-entry-name-button").first().click();140 await popup.waitForTimeout(100);141 const usernameField = popup.locator(".peach-field-value").first();142 await usernameField.waitFor({ state: "visible", timeout: 3000 });143 const elapsed = Date.now() - start;144 expect(elapsed).toBeLessThan(800);145 await ctx.close();146 });147});148149test.describe("Typography & Layout", () => {150 let ctx: BrowserContext;151 test.afterEach(async () => { await ctx.close(); });152 test.beforeEach(async () => { ctx = await makeContext(); });153154 test("VTL01 - font-size consistent in popup", async () => {155 const { popup } = await setupVault(ctx);156 const sizes = await popup.evaluate(() => {157 const elements = document.querySelectorAll("*");158 const sizes: number[] = [];159 elements.forEach(el => {160 const fs = window.getComputedStyle(el).fontSize;161 if (fs) sizes.push(parseFloat(fs));162 });163 return [...new Set(sizes)].slice(0, 20);164 });165 expect(sizes.length).toBeGreaterThan(0);166 await ctx.close();167 });168169 test("VTL02 - no font loaded from Google CDN", async () => {170 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>171 f.filter(f => f.endsWith(".css") || f.endsWith(".html"))172 );173 let foundGoogleFont = false;174 for (const file of distFiles) {175 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8").catch(() => "");176 const htmlContent = await fs.readFile(join(__dirname, "..", "dist", "src", "popup", "index.html"), "utf-8").catch(() => "");177 const combined = content + htmlContent;178 if (combined.includes("fonts.googleapis.com") || combined.includes("fonts.gstatic.com")) {179 foundGoogleFont = true;180 break;181 }182 }183 expect(foundGoogleFont).toBeFalsy();184 await ctx.close();185 });186187 test("VTL03 - font-display uses system fonts", async () => {188 const { popup } = await setupVault(ctx);189 const fontFamily = await popup.evaluate(() => {190 return window.getComputedStyle(document.body).fontFamily;191 });192 expect(fontFamily).toBeTruthy();193 await ctx.close();194 });195196 test("VTL04 - popup width is fixed at 320px", async () => {197 const { popup } = await setupVault(ctx);198 const width = await popup.evaluate(() => document.body?.parentElement?.clientWidth || 0);199 expect(width).toBe(320);200 await ctx.close();201 });202203 test("VTL05 - content area has proper padding", async () => {204 const { popup } = await setupVault(ctx);205 const contentFrame = popup.locator(".peach-content-frame");206 const padding = await contentFrame.evaluate((el: Element) => window.getComputedStyle(el).padding);207 expect(padding).toBeTruthy();208 await ctx.close();209 });210211 test("VTL06 - no horizontal overflow on body", async () => {212 const { popup } = await setupVault(ctx);213 const overflow = await popup.evaluate(() => {214 return window.getComputedStyle(document.body).overflowX;215 });216 expect(overflow).toBe("hidden");217 await ctx.close();218 });219220 test("VTL07 - entry list items have consistent height", async () => {221 const { popup } = await setupVault(ctx);222 for (const name of ["H1", "H2", "H3"]) {223 await popup.locator('input[placeholder="search vault..."]').click();224 await popup.getByRole("button", { name: /new entry/i }).click();225 await popup.locator(".peach-new-name-input").fill(name);226 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);227 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);228 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");229 await popup.getByRole("button", { name: /create/i }).click();230 await popup.waitForTimeout(800);231 }232 const rowHeights = await popup.evaluate(() => {233 const rows = document.querySelectorAll(".peach-entry-row");234 return Array.from(rows).map(r => r.getBoundingClientRect().height);235 });236 const unique = [...new Set(rowHeights)];237 expect(unique.length).toBeLessThanOrEqual(2);238 await ctx.close();239 });240241 test("VTL08 - search bar is always visible", async () => {242 const { popup } = await setupVault(ctx);243 const search = popup.locator(".peach-search-bar, .peach-search-input");244 await expect(search).toBeVisible();245 await ctx.close();246 });247});248249test.describe("Scrollbar Styling", () => {250 let ctx: BrowserContext;251 test.afterEach(async () => { await ctx.close(); });252 test.beforeEach(async () => { ctx = await makeContext(); });253254 test("VSB01 - scrollbar width is 3px or less", async () => {255 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>256 f.filter(f => f.endsWith(".css"))257 );258 for (const file of cssFiles) {259 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");260 const widthMatch = content.match(/::-webkit-scrollbar\s*\{[\s\S]*?width:\s*(\d+)px/);261 if (widthMatch) {262 expect(parseInt(widthMatch[1])).toBeLessThanOrEqual(4);263 }264 }265 await ctx.close();266 });267268 test("VSB02 - scrollbar has subtle color (not bright)", async () => {269 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>270 f.filter(f => f.endsWith(".css"))271 );272 for (const file of cssFiles) {273 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");274 if (content.includes("::-webkit-scrollbar")) {275 const hasVar = content.includes("var(--t");276 expect(hasVar).toBeTruthy();277 }278 }279 await ctx.close();280 });281282 test("VSB03 - scrollbar uses border-radius (rounded)", async () => {283 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>284 f.filter(f => f.endsWith(".css"))285 );286 for (const file of cssFiles) {287 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");288 if (content.includes("::-webkit-scrollbar-thumb")) {289 const hasBorderRadius = content.includes("border-radius") || content.includes("border-radius:");290 expect(hasBorderRadius).toBeTruthy();291 }292 }293 await ctx.close();294 });295296 test("VSB04 - content frame scrolls when overflow", async () => {297 const { popup } = await setupVault(ctx);298 for (let i = 0; i < 20; i++) {299 await popup.locator('input[placeholder="search vault..."]').click();300 await popup.getByRole("button", { name: /new entry/i }).click();301 await popup.locator(".peach-new-name-input").fill(`ScrollTest${i}`);302 await popup.locator('input[placeholder="domain.com"]').fill(`https://scroll${i}.com`);303 await popup.locator('input[placeholder="username or email"]').fill(`scroll${i}@user.com`);304 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");305 await popup.getByRole("button", { name: /create/i }).click();306 await popup.waitForTimeout(400);307 }308 const scroller = popup.locator(".peach-content-frame");309 const canScroll = await scroller.evaluate((el: Element) => el.scrollHeight > el.clientHeight);310 expect(canScroll).toBeTruthy();311 await ctx.close();312 });313314 test("VSB05 - scrollbar appears on hover", async () => {315 const { popup } = await setupVault(ctx);316 for (let i = 0; i < 15; i++) {317 await popup.locator('input[placeholder="search vault..."]').click();318 await popup.getByRole("button", { name: /new entry/i }).click();319 await popup.locator(".peach-new-name-input").fill(`HoverScroll${i}`);320 await popup.locator('input[placeholder="domain.com"]').fill(`https://hover${i}.com`);321 await popup.locator('input[placeholder="username or email"]').fill(`hover${i}@user.com`);322 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");323 await popup.getByRole("button", { name: /create/i }).click();324 await popup.waitForTimeout(400);325 }326 const scroller = popup.locator(".peach-content-frame");327 await scroller.hover();328 await popup.waitForTimeout(200);329 const scrollThumb = scroller.locator("::-webkit-scrollbar-thumb");330 await scrollThumb.isVisible().catch(() => false);331 await ctx.close();332 });333});334335test.describe("Color & Theme Compliance", () => {336 let ctx: BrowserContext;337 test.afterEach(async () => { await ctx.close(); });338 test.beforeEach(async () => { ctx = await makeContext(); });339340 test("VCT01 - all colors use CSS variables", async () => {341 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>342 f.filter(f => f.endsWith(".css"))343 );344 const hardcodedColors: string[] = [];345 for (const file of cssFiles) {346 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");347 const hexColors = content.match(/#[0-9a-fA-F]{3,8}/g) || [];348 for (const color of hexColors) {349 if (!content.includes(`--${color.replace("#", "")}`) &&350 !color.match(/^#000$|^#fff$|^#fff0000$/i)) {351 const propMatch = content.substring(content.indexOf(color) - 50, content.indexOf(color)).match(/[a-z-]+:\s*[^;]*/i);352 if (propMatch && !propMatch[0].includes("var(")) {353 hardcodedColors.push(`${file}: ${color}`);354 }355 }356 }357 }358 expect(hardcodedColors.slice(0, 5)).toHaveLength(0);359 await ctx.close();360 });361362 test("VCT02 - accent color variable (--ac) is used", async () => {363 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>364 f.filter(f => f.endsWith(".css"))365 );366 let foundAC = false;367 for (const file of cssFiles) {368 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");369 if (content.includes("--ac") || content.includes("var(--ac)")) {370 foundAC = true;371 break;372 }373 }374 expect(foundAC).toBeTruthy();375 await ctx.close();376 });377378 test("VCT03 - tint variables (--t0 through --t4) all defined", async () => {379 await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>380 f.filter(f => f.endsWith(".css"))381 );382 const varFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>383 f.filter(f => f.includes("vars") || f.includes("variables"))384 );385 const varsCss = varFiles.length > 0386 ? await fs.readFile(join(__dirname, "..", "dist", "assets", varFiles[0]), "utf-8").catch(() => "")387 : "";388 if (varsCss) {389 expect(varsCss).toContain("--t0");390 expect(varsCss).toContain("--t1");391 expect(varsCss).toContain("--t2");392 expect(varsCss).toContain("--t3");393 expect(varsCss).toContain("--t4");394 }395 await ctx.close();396 });397398 test("VCT04 - no inline styles use !important", async () => {399 const { popup } = await setupVault(ctx);400 const importantStyles = await popup.evaluate(() => {401 const elements = document.querySelectorAll("*");402 let count = 0;403 elements.forEach(el => {404 const style = (el as HTMLElement).style;405 for (const prop of Object.keys(style)) {406 if ((style as any)[prop]?.includes("!important")) {407 count++;408 }409 }410 });411 return count;412 });413 expect(importantStyles).toBe(0);414 await ctx.close();415 });416417 test("VCT05 - focus ring uses accent color", async () => {418 const { popup } = await setupVault(ctx);419 await popup.locator('input[placeholder="search vault..."]').click();420 await popup.keyboard.press("Tab");421 await popup.waitForTimeout(100);422 const outlineColor = await popup.evaluate(() => {423 const el = document.activeElement as HTMLElement;424 return window.getComputedStyle(el).outlineColor;425 });426 expect(outlineColor).toBeTruthy();427 await ctx.close();428 });429});430431test.describe("Performance Metrics", () => {432 let ctx: BrowserContext;433 test.afterEach(async () => { await ctx.close(); });434 test.beforeEach(async () => { ctx = await makeContext(); });435436 test("VPM01 - page renders within 1 second", async () => {437 const extId = await waitForSW(ctx);438 const start = Date.now();439 const newPage = await ctx.newPage();440 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);441 await newPage.waitForTimeout(3000);442 const elapsed = Date.now() - start;443 expect(elapsed).toBeLessThan(4000);444 await ctx.close();445 });446447 test("VPM02 - vault unlocks within 3 seconds", async () => {448 await setupVault(ctx);449 const newPage = await ctx.newPage();450 const extId = await waitForSW(ctx);451 await newPage.goto(`chrome-extension://${extId}/src/popup/index.html`);452 await newPage.waitForTimeout(2000);453 const start = Date.now();454 await newPage.locator("#master-password").fill("password123");455 await newPage.getByRole("button", { name: /unlock/i }).click();456 await newPage.waitForTimeout(500);457 const entries = newPage.locator("[class*=entry]");458 await entries.first().waitFor({ state: "visible", timeout: 5000 });459 const elapsed = Date.now() - start;460 expect(elapsed).toBeLessThan(4000);461 await ctx.close();462 });463464 test("VPM03 - 50 entries render without lag", async () => {465 const { popup } = await setupVault(ctx);466 for (let i = 0; i < 50; i++) {467 await popup.locator('input[placeholder="search vault..."]').click();468 await popup.getByRole("button", { name: /new entry/i }).click();469 await popup.locator(".peach-new-name-input").fill(`Perf${i}`);470 await popup.locator('input[placeholder="domain.com"]').fill(`https://perf${i}.com`);471 await popup.locator('input[placeholder="username or email"]').fill(`perf${i}@user.com`);472 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");473 await popup.getByRole("button", { name: /create/i }).click();474 await popup.waitForTimeout(200);475 }476 const start = Date.now();477 await popup.locator('input[placeholder="search vault..."]').click();478 await popup.locator('input[placeholder="search vault..."]').fill("Perf25");479 await popup.waitForTimeout(500);480 const elapsed = Date.now() - start;481 expect(elapsed).toBeLessThan(1000);482 await ctx.close();483 });484485 test("VPM04 - CSS file size is reasonable (<100KB)", async () => {486 const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>487 f.filter(f => f.endsWith(".css"))488 );489 for (const file of cssFiles) {490 const stats = await fs.stat(join(__dirname, "..", "dist", "assets", file));491 expect(stats.size).toBeLessThan(100 * 1024);492 }493 await ctx.close();494 });495});496497test.describe("Dark Theme Consistency", () => {498 let ctx: BrowserContext;499 test.afterEach(async () => { await ctx.close(); });500 test.beforeEach(async () => { ctx = await makeContext(); });501502 test("VDT01 - background uses dark theme color", async () => {503 const { popup } = await setupVault(ctx);504 const bg = await popup.evaluate(() => window.getComputedStyle(document.body).backgroundColor);505 expect(bg).toBeTruthy();506 await ctx.close();507 });508509 test("VDT02 - text contrast meets WCAG AA", async () => {510 const { popup } = await setupVault(ctx);511 const contrast = await popup.evaluate(() => {512 const bg = window.getComputedStyle(document.body).backgroundColor;513 const text = window.getComputedStyle(document.body).color;514 return { bg, text };515 });516 expect(contrast.bg).toBeTruthy();517 expect(contrast.text).toBeTruthy();518 await ctx.close();519 });520521 test("VDT03 - input fields have dark background", async () => {522 const { popup } = await setupVault(ctx);523 const inputBg = await popup.evaluate(() => {524 const input = document.querySelector("input") as HTMLInputElement;525 return input ? window.getComputedStyle(input).backgroundColor : null;526 });527 expect(inputBg).toBeTruthy();528 await ctx.close();529 });530531 test("VDT04 - buttons have proper contrast", async () => {532 const { popup } = await setupVault(ctx);533 const button = popup.locator("button").first();534 const contrast = await button.evaluate((el: Element) => {535 const styles = window.getComputedStyle(el);536 return {537 color: styles.color,538 backgroundColor: styles.backgroundColor539 };540 });541 expect(contrast.color).toBeTruthy();542 await ctx.close();543 });544545 test("VDT05 - overlay backdrop is semi-transparent dark", async () => {546 const { popup } = await setupVault(ctx);547 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();548 await popup.waitForTimeout(500);549 const overlay = popup.locator(".peach-overlay-view");550 const bg = await overlay.evaluate((el: Element) => window.getComputedStyle(el).backgroundColor);551 expect(bg).toBeTruthy();552 await ctx.close();553 });554});555556test.describe("Spacing & Layout", () => {557 let ctx: BrowserContext;558 test.afterEach(async () => { await ctx.close(); });559 test.beforeEach(async () => { ctx = await makeContext(); });560561 test("VSP01 - consistent 8px grid spacing", async () => {562 const { popup } = await setupVault(ctx);563 const spacings = await popup.evaluate(() => {564 const elements = document.querySelectorAll(".peach-stage > *");565 return Array.from(elements).map((el: Element) => {566 const style = window.getComputedStyle(el);567 return parseFloat(style.gap || style.marginBottom || "0");568 }).filter(v => v > 0);569 });570 const allAligned = spacings.every((s: number) => s % 4 === 0);571 expect(allAligned || spacings.length === 0).toBeTruthy();572 await ctx.close();573 });574575 test("VSP02 - entry row padding is consistent", async () => {576 const { popup } = await setupVault(ctx);577 for (const name of ["Space1", "Space2", "Space3"]) {578 await popup.locator('input[placeholder="search vault..."]').click();579 await popup.getByRole("button", { name: /new entry/i }).click();580 await popup.locator(".peach-new-name-input").fill(name);581 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);582 await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);583 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");584 await popup.getByRole("button", { name: /create/i }).click();585 await popup.waitForTimeout(800);586 }587 const paddings = await popup.evaluate(() => {588 const rows = document.querySelectorAll(".peach-entry-row");589 return Array.from(rows).map(r => window.getComputedStyle(r).padding);590 });591 const unique = [...new Set(paddings)];592 expect(unique.length).toBeLessThanOrEqual(3);593 await ctx.close();594 });595596 test("VSP03 - icon buttons have proper hit area", async () => {597 const { popup } = await setupVault(ctx);598 const settingsBtn = popup.locator(".peach-icon-btn[aria-label='Settings']");599 const box = await settingsBtn.boundingBox();600 expect(box?.width).toBeGreaterThanOrEqual(24);601 expect(box?.height).toBeGreaterThanOrEqual(24);602 await ctx.close();603 });604605 test("VSP04 - content area has no unexpected clipping", async () => {606 const { popup } = await setupVault(ctx);607 for (let i = 0; i < 10; i++) {608 await popup.locator('input[placeholder="search vault..."]').click();609 await popup.getByRole("button", { name: /new entry/i }).click();610 await popup.locator(".peach-new-name-input").fill(`Clip${i}`);611 await popup.locator('input[placeholder="domain.com"]').fill(`https://clip${i}.com`);612 await popup.locator('input[placeholder="username or email"]').fill(`clip${i}@user.com`);613 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");614 await popup.getByRole("button", { name: /create/i }).click();615 await popup.waitForTimeout(500);616 }617 const stage = popup.locator(".peach-stage");618 const stageBox = await stage.boundingBox();619 const lastEntry = popup.locator(".peach-entry-row").last();620 const entryBox = await lastEntry.boundingBox();621 expect(entryBox?.bottom).toBeLessThanOrEqual((stageBox?.bottom ?? 0) + 10);622 await ctx.close();623 });624});