import { test, expect } from "@playwright/test"; import { pw_kanban_login } from "../../../../frontend/functions/browser/pw_kanban_login"; const USER = process.env.KANBAN_USER || "e2e_user"; const PWD = process.env.KANBAN_PWD || "e2e_test_pw_2026"; /** * Issue followup: drag lag. * Capture per-frame durations via requestAnimationFrame while a card is dragged * across reorder positions inside a populated column. Asserts p50 < 32ms and * max < 120ms so a regression visibly slower than ~30 fps fails the suite. * * Read each measurement printed to console to track changes over time. */ test.describe("kanban drag perf", () => { test("reorder inside HACIENDO does not drop below 30 fps", async ({ page }) => { await page.goto("/"); await pw_kanban_login(page, { username: USER, password: PWD }); // Wait until at least one card is mounted. await page.waitForSelector("[data-card-id]", { timeout: 10_000 }); // Inject a tiny frame-time recorder. await page.evaluate(() => { const w = window as unknown as { _frames: number[]; _capturing: boolean; _startCapture: () => void; _stopCapture: () => number[]; }; w._frames = []; w._capturing = false; let prev = 0; const tick = (t: number) => { if (!w._capturing) return; if (prev !== 0) w._frames.push(t - prev); prev = t; requestAnimationFrame(tick); }; w._startCapture = () => { w._frames = []; w._capturing = true; prev = 0; requestAnimationFrame(tick); }; w._stopCapture = () => { w._capturing = false; return w._frames.slice(); }; }); // Pick the column with the MOST cards (worst-case reorder cost). const target = await page.evaluate(() => { const cols = Array.from(document.querySelectorAll("[data-column-id]")); let best: { columnId: string | null; cardIds: string[] } | null = null; for (const col of cols) { const cards = Array.from(col.querySelectorAll("[data-card-id]")); if (!best || cards.length > best.cardIds.length) { best = { columnId: col.getAttribute("data-column-id"), cardIds: cards .map((c) => c.getAttribute("data-card-id")) .filter((x): x is string => x !== null), }; } } return best && best.cardIds.length >= 3 ? best : null; }); if (!target) test.skip(true, "need a column with >= 3 cards"); const firstId = target!.cardIds[0]!; const lastId = target!.cardIds[target!.cardIds.length - 1]!; const source = page.locator(`[data-card-id="${firstId}"]`); const targetEl = page.locator(`[data-card-id="${lastId}"]`); const sb = await source.boundingBox(); const tb = await targetEl.boundingBox(); if (!sb || !tb) throw new Error("no bounding box"); const sx = sb.x + sb.width / 2; const sy = sb.y + sb.height / 2; const tx = tb.x + tb.width / 2; const ty = tb.y + tb.height / 2; await page.mouse.move(sx, sy); await page.mouse.down(); // dnd-kit pointer-sensor activation threshold: 8px; nudge horizontally first. await page.mouse.move(sx + 12, sy, { steps: 2 }); // Probe how many KanbanCard renders happen during the drag. await page.evaluate(() => { const w = window as unknown as { _cardRenderCount: number; _cardRenderProbe: boolean }; w._cardRenderCount = 0; w._cardRenderProbe = true; }); await page.evaluate(() => (window as unknown as { _startCapture: () => void })._startCapture()); // Move slowly across the column to trigger reorder swaps; steps=40 gives // dnd-kit time to recompute positions. await page.mouse.move(tx, ty, { steps: 40 }); // Hover so any final layout animation captures into the trace. await page.waitForTimeout(120); const frames = (await page.evaluate(() => (window as unknown as { _stopCapture: () => number[] })._stopCapture() )) as number[]; const renderCount = (await page.evaluate( () => (window as unknown as { _cardRenderCount: number })._cardRenderCount )) as number; const bodyCount = (await page.evaluate( () => (window as unknown as { _cardBodyRenderCount: number })._cardBodyRenderCount || 0 )) as number; console.log(`drag-perf wrapper-renders=${renderCount} body-renders=${bodyCount} over ${frames.length} frames`); await page.mouse.up(); const sorted = [...frames].sort((a, b) => a - b); const p50 = sorted[Math.floor(sorted.length * 0.5)] ?? 0; const p95 = sorted[Math.floor(sorted.length * 0.95)] ?? 0; const max = sorted.length > 0 ? sorted[sorted.length - 1] : 0; const avg = sorted.reduce((a, b) => a + b, 0) / Math.max(1, sorted.length); console.log( `drag-perf frames=${sorted.length} avg=${avg.toFixed(1)}ms p50=${p50.toFixed(1)}ms p95=${p95.toFixed(1)}ms max=${max.toFixed(1)}ms` ); // Save a stable artefact so we can compare runs. test.info().annotations.push({ type: "drag-perf", description: JSON.stringify({ count: sorted.length, avg, p50, p95, max }), }); // Thresholds tuned tras separar el body memoizado (issue dnd-lag-fix // followup). Pre-fix: p95=83ms / max=117ms. Post-fix: p95=33 / max=33. expect(p50).toBeLessThan(20); expect(p95).toBeLessThan(50); expect(max).toBeLessThan(60); // Body memoizado: durante el drag no debe re-renderizar. expect(bodyCount).toBeLessThan(5); }); });