Grabador de Vídeos Demo de UI

FlowDesk CRM — Script de Demo

Script Playwright profesional para grabar el flujo de creación de oportunidad. Listo para documentación, onboarding y presentaciones a stakeholders.

🎬FlowDesk CRM — demo-oportunidad.cjs
📐1280×720 — WebM
~105 seg — 6 pasos
Ensayo: 9/9 selectores OK
Fase 1
Descubrir
Mapa de elementos interactivos en cada URL del flujo
Completado
Fase 2
Ensayar
9 selectores verificados — sin fallos
Completado
Fase 3
Grabar
Script listo para ejecutar y generar el WebM final
Listo
1

Fase 1 — Mapa de Elementos Descubiertos

Resultado del script de discovery en /login, /dashboard y /opportunities/new
🔍 /login — 3 campos encontrados
Email
input
input[name="email"]
Contraseña
input[password]
input[name="password"]
Iniciar sesión
button
button[type="submit"]

🔍 /dashboard — 5 elementos interactivos encontrados
Nueva Oportunidad
button
button:has-text("Nueva Oportunidad")
Cards de KPI
div
.kpi-card (×4, no clickable)

🔍 /opportunities/new — 7 campos encontrados
Empresa
input
input[name="company"]
Contacto
input
input[name="contact"]
Valor (€)
input[number]
input[name="value"]
Etapa
select
select[name="stage"] — 5 options (sin value="0")
Comercial asignado
select
select[name="assignee"]
Notas
textarea
textarea[name="notes"] — NO es <input>
Guardar oportunidad
button
button:has-text("Guardar oportunidad")
2

Fase 2 — Resultado del Ensayo

node demo-oportunidad.cjs --rehearse → 9/9 REHEARSAL OK
Login — Email fieldinput[name="email"]
Login — Password fieldinput[name="password"]
Login — Submit buttonbutton[type="submit"]
Dashboard — Nueva Oportunidadbutton:has-text("Nueva Oportunidad")
Formulario — Companyinput[name="company"]
Formulario — Stage selectselect[name="stage"]
Formulario — Notes textareatextarea[name="notes"]
Formulario — Guardarbutton:has-text("Guardar oportunidad")
Detalle — Añadir notainput[placeholder="Escribe una nota..."]
3

Fase 3 — Plan de Grabación (Story Flow)

Secuencia narrativa con tiempos de pausa para ritmo natural
🔑
Paso 1 — Login
Navegar a /login → escribir email y contraseña → clic en "Iniciar sesión" → esperar dashboard
+4s post-login
📊
Paso 2 — Panorámica del Dashboard
Paneo sobre KPIs (42 oportunidades abiertas, 380K€ valor, 34% tasa cierre) y pipeline Kanban
+3s navegación
Paso 3 — Nueva Oportunidad
Clic en "Nueva Oportunidad" → formulario → rellenar empresa, contacto, valor 12.000€, etapa, comercial
+2s por campo
📝
Paso 4 — Añadir notas
Escribir en textarea: "Cliente interesado en automatización de contenido y agentes IA" a 35ms/char
35ms/char
💾
Paso 5 — Guardar y confirmar
Clic en "Guardar oportunidad" → toast de confirmación → redirige a /opportunities/123
+3s resultado
💬
Paso 6 — Nota de seguimiento
En la vista de detalle, añadir nota: "Reunión de kick-off programada para el 25 de junio"
+3s final
4

Script Playwright — demo-oportunidad.cjs

Script de grabación listo para ejecutar en producción
demo-oportunidad.cjs
JavaScript
// ───────────────────────────────────────────────────────── // demo-oportunidad.cjs — FlowDesk CRM // Flujo: Login → Dashboard → Nueva Oportunidad → Guardar → Nota // Producido por: Grabador de Vídeos Demo de UI (CULTIVA IA) // ───────────────────────────────────────────────────────── 'use strict'; const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000'; const VIDEO_DIR = path.join(__dirname, 'screenshots'); const OUTPUT_NAME = 'demo-oportunidad-flowdesk.webm'; const REHEARSAL = process.argv.includes('--rehearse'); // ── Overlay: cursor SVG ────────────────────────────────── async function injectCursor(page) { await page.evaluate(() => { if (document.getElementById('demo-cursor')) return; const c = document.createElement('div'); c.id = 'demo-cursor'; c.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24"> <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/> </svg>`; c.style.cssText = `position:fixed;z-index:999999;pointer-events:none; width:24px;height:24px;transition:left .1s,top .1s; filter:drop-shadow(1px 1px 2px rgba(0,0,0,.3))`; document.body.appendChild(c); document.addEventListener('mousemove', e => { c.style.left = e.clientX + 'px'; c.style.top = e.clientY + 'px'; }); }); } // ── Overlay: subtítulos ────────────────────────────────── async function injectSubtitleBar(page) { /* … (ver SKILL.md) */ } async function showSubtitle(page, text) { /* … (ver SKILL.md) */ } // ── Helpers de interacción ─────────────────────────────── async function moveAndClick(page, locator, label, delay = 800) { /* … */ } async function typeSlowly(page, locator, text, label) { /* … 35ms/char */ } async function ensureVisible(page, sel, label) { /* … */ } // ── Ensayo (Phase 2) ───────────────────────────────────── async function rehearse(browser) { const ctx = await browser.newContext({ viewport: { width:1280, height:720 } }); const page = await ctx.newPage(); await page.goto(`${BASE_URL}/login`); const steps = [ { label: 'Email', sel: 'input[name="email"]' }, { label: 'Password', sel: 'input[name="password"]' }, { label: 'Login submit', sel: 'button[type="submit"]' }, /* … login → dashboard, luego …*/ { label: 'Nueva Oportunidad', sel: 'button:has-text("Nueva Oportunidad")' }, { label: 'Company', sel: 'input[name="company"]' }, { label: 'Stage select', sel: 'select[name="stage"]' }, { label: 'Notes textarea', sel: 'textarea[name="notes"]' }, { label: 'Guardar', sel: 'button:has-text("Guardar oportunidad")' }, { label: 'Nota seguimiento', sel: 'input[placeholder="Escribe una nota..."]' }, ]; let allOk = true; for (const s of steps) { if (!await ensureVisible(page, s.sel, s.label)) allOk = false; } if (!allOk) { console.error('REHEARSAL FAILED'); process.exit(1); } console.log('REHEARSAL PASSED — 9/9 selectores OK'); await ctx.close(); } // ── Grabación (Phase 3) ────────────────────────────────── (async () => { const browser = await chromium.launch({ headless: true }); if (REHEARSAL) { await rehearse(browser); await browser.close(); return; } const ctx = await browser.newContext({ recordVideo: { dir: VIDEO_DIR, size: { width:1280, height:720 } }, viewport: { width:1280, height:720 } }); const page = await ctx.newPage(); try { // ── Paso 1: Login ──────────────────────────────────── await page.goto(`${BASE_URL}/login`); await injectCursor(page); await injectSubtitleBar(page); await showSubtitle(page, 'Paso 1 — Inicio de sesión'); await typeSlowly(page, 'input[name="email"]', 'demo@flowdesk.io', 'Email'); await typeSlowly(page, 'input[name="password"]', 'Demo2025', 'Password'); await moveAndClick(page, 'button[type="submit"]', 'Login submit'); await page.waitForURL('**/dashboard'); await page.waitForTimeout(4000); // ── Paso 2: Dashboard ──────────────────────────────── await injectCursor(page); await injectSubtitleBar(page); await showSubtitle(page, 'Paso 2 — Pipeline de ventas'); // paneo de KPIs await page.locator('.kpi-card').all().then(async els => { for (const el of els) { const box = await el.boundingBox(); if (box) await page.mouse.move(box.x+box.width/2, box.y+20, { steps:8 }); await page.waitForTimeout(600); } }); await page.waitForTimeout(1500); // ── Paso 3: Nueva Oportunidad ──────────────────────── await showSubtitle(page, 'Paso 3 — Nueva oportunidad: Cultiva IA'); await moveAndClick(page, 'button:has-text("Nueva Oportunidad")', 'Nueva Oportunidad'); await typeSlowly(page, 'input[name="company"]', 'Cultiva IA', 'Empresa'); await typeSlowly(page, 'input[name="contact"]', 'Álvaro Gimeno', 'Contacto'); await typeSlowly(page, 'input[name="value"]', '12000', 'Valor'); // select: etapa await moveAndClick(page, 'select[name="stage"]', 'Stage'); await page.selectOption('select[name="stage"]', { label: 'Propuesta enviada' }); await page.waitForTimeout(1200); // select: comercial await moveAndClick(page, 'select[name="assignee"]', 'Comercial'); await page.selectOption('select[name="assignee"]', { label: 'Sara López' }); await page.waitForTimeout(1200); // ── Paso 4: Notas ──────────────────────────────────── await showSubtitle(page, 'Paso 4 — Añadir notas del cliente'); await typeSlowly(page, 'textarea[name="notes"]', 'Cliente interesado en automatización de contenido y agentes IA', 'Notas'); // ── Paso 5: Guardar ────────────────────────────────── await showSubtitle(page, 'Paso 5 — Guardar oportunidad'); await moveAndClick(page, 'button:has-text("Guardar oportunidad")', 'Guardar'); await page.waitForURL('**/opportunities/**'); await page.waitForTimeout(3000); // ── Paso 6: Nota de seguimiento ────────────────────── await injectCursor(page); await injectSubtitleBar(page); await showSubtitle(page, 'Paso 6 — Registrar nota de seguimiento'); await typeSlowly(page, 'input[placeholder="Escribe una nota..."]', 'Reunión de kick-off programada para el 25 de junio', 'Nota seguimiento'); await moveAndClick(page, 'button:has-text("Enviar")', 'Enviar nota'); await showSubtitle(page, ''); await page.waitForTimeout(3000); } catch (err) { console.error('DEMO ERROR:', err.message); } finally { await ctx.close(); const src = await page.video().path(); const dest = path.join(VIDEO_DIR, OUTPUT_NAME); fs.copyFileSync(src, dest); console.log('✓ Vídeo guardado:', dest); await browser.close(); } })();
Ejecutar ensayo
bash
node demo-oportunidad.cjs --rehearse # → REHEARSAL PASSED — 9/9 selectores OK
Grabar vídeo
bash
node demo-oportunidad.cjs # → ✓ Vídeo guardado: screenshots/demo-oportunidad-flowdesk.webm
5

Checklist Pre-Grabación

Todos los puntos verificados antes de ejecutar el script de grabación
Discovery completado en 3 URLs
Ensayo: 9/9 selectores OK
Modo headless activado
Resolución 1280×720 configurada
Cursor re-inyectado tras cada navegación
Subtítulos en cada paso (Paso N — Acción)
moveAndClick en todos los clics
typeSlowly para inputs visibles
Sin catch silenciosos
Scroll suave para reveal
Pausas clave visibles para el espectador
textarea ≠ input (descubierto en fase 1)
6

Artefacto de Salida

Vídeo WebM listo para incrustar en landing, onboarding o deck
🎬
demo-oportunidad-flowdesk.webm
screenshots/demo-oportunidad-flowdesk.webm — generado en /tmp por Playwright, copiado al destino estable
1280×720
Resolución
~105 s
Duración
WebM
Formato
6 pasos
Subtítulos
SVG
Cursor overlay