Communitygithub.com

UI-Demo-Video-Recorder

Zeichne polierte UI-Demo-Videos mit Playwright auf. Verwende es, wenn der Benutzer ein Demo-, Walkthrough-, Bildschirmaufzeichnungs- oder Tutorial-Video einer Webanwendung erstellen möchte. Produziert WebM-Videos mit sichtbarem Cursor, natürlichem Tempo und professionellem Gefühl.

Was ist UI-Demo-Video-Recorder?

Diese Fähigkeit ermöglicht es einem KI-Agenten, polierte Demo-Videos von Webanwendungen mit Playwright zu erstellen. Sie führt den Agenten durch einen strukturierten Dreiphasenprozess: Entdecken, Proben und Aufnehmen. In der Entdeckungsphase untersucht der Agent die Zielseiten, um interaktive Elemente wie Formularfelder, Dropdowns und Schaltflächen zu identifizieren, und erstellt eine Feldkarte für korrekte Selektoren. Die Probenphase überprüft, ob alle Selektoren sichtbar sind, um stille Fehler während der Aufnahme zu vermeiden. Die Aufnahmephase erzeugt WebM-Videos mit einer eingebetteten Cursor-Überlagerung (einen SVG-Pfeil, der den Mausbewegungen folgt), natürlichem Tempo (Pausen zwischen Aktionen) und einer Erzählstruktur (Einstieg, Kontext, Aktion, Variation, Ergebnis). Sie unterstützt Untertitelbalken, sanftes Scrollen und Dashboard-Panning. Die Fähigkeit ist plattformunabhängig und kann mit jeder Webanwendung verwendet werden, die in einem headless Playwright-Browser ausgeführt wird. Sie eignet sich ideal für die Erstellung von Videos für Dokumentation, Onboarding oder Präsentationen vor Stakeholdern.

Funktioniert mit~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/alhurwitz/alberts-agentic-arsenal/tree/HEAD/skills/ui-demo

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Was macht UI-Demo-Video-Recorder?

Zeichne polierte Demo-Videos von Webanwendungen mit Playwrights Videoaufzeichnung auf, einschließlich einer eingebetteten Cursor-Überlagerung, natürlichem Tempo und einer erzählerischen Struktur.

Wann verwenden

  • Der Benutzer fragt nach einem "Demo-Video", "Bildschirmaufzeichnung", "Walkthrough" oder "Tutorial"
  • Der Benutzer möchte eine Funktion oder einen Workflow visuell präsentieren
  • Der Benutzer benötigt ein Video für Dokumentation, Onboarding oder Präsentationen vor Stakeholdern

Dreiphasenprozess

Jede Demo durchläuft drei Phasen: Entdecken -> Proben -> Aufnehmen. Niemals direkt zur Aufnahme springen.


Phase 1: Entdecken

Bevor ein Skript geschrieben wird, erkunde die Zielseiten, um zu verstehen, was tatsächlich vorhanden ist.

Warum

Man kann nichts skripten, was man nicht gesehen hat. Felder können <input> und nicht <textarea> sein, Dropdowns können benutzerdefinierte Komponenten und nicht <select> sein, und Kommentarfelder können @Erwähnungen oder #Tags unterstützen. Annahmen brechen Aufnahmen stillschweigend.

Wie

Navigiere zu jeder Seite im Workflow und gib ihre interaktiven Elemente aus:

// Führe dies für jede Seite im Workflow AUS, bevor das Demo-Skript geschrieben wird
const fields = await page.evaluate(() => {
  const els = [];
  document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
    if (el.offsetParent !== null) {
      els.push({
        tag: el.tagName,
        type: el.type || '',
        name: el.name || '',
        placeholder: el.placeholder || '',
        text: el.textContent?.trim().substring(0, 40) || '',
        contentEditable: el.contentEditable === 'true',
        role: el.getAttribute('role') || '',
      });
    }
  });
  return els;
});
console.log(JSON.stringify(fields, null, 2));

Worauf achten

  • Formularfelder: Sind es <select>, <input>, benutzerdefinierte Dropdowns oder Comboboxen?
  • Select-Optionen: Gib Optionswerte UND Text aus. Platzhalter haben oft value="0" oder value="", was nicht leer aussieht. Verwende Array.from(el.options).map(o => ({ value: o.value, text: o.text })). Überspringe Optionen, deren Text "Auswählen" enthält oder deren Wert "0" ist.
  • Rich Text: Unterstützt das Kommentarfeld @Erwähnungen, #Tags, Markdown oder Emojis? Überprüfe den Platzhaltertext.
  • Pflichtfelder: Welche Felder blockieren die Formularübermittlung? Überprüfe required, * in Labels und versuche, das Formular leer abzusenden, um Validierungsfehler zu sehen.
  • Dynamischer Inhalt: Werden Felder erst sichtbar, nachdem andere Felder ausgefüllt wurden?
  • Schaltflächenbeschriftungen: Exakter Text wie "Absenden", "Anfrage absenden" oder "Senden".
  • Tabellenkopfzeilen: Ordne bei tabellengesteuerten Modalen jedes input[type="number"] seiner Kopfzeile zu, anstatt anzunehmen, dass alle numerischen Eingaben dasselbe bedeuten.

Ausgabe

Eine Feldkarte für jede Seite, die verwendet wird, um korrekte Selektoren im Skript zu schreiben. Beispiel:

/purchase-requests/new:
  - Budget Code: <select> (erstes Select auf der Seite, 4 Optionen)
  - Gewünschte Lieferung: <input type="date">
  - Kontext: <textarea> (nicht input)
  - BOM-Tabelle: inline-editable Zellen mit span.cursor-pointer -> input-Muster
  - Absenden: <button> text="Absenden"

/purchase-requests/N (Detail):
  - Kommentar: <input placeholder="Nachricht eingeben..."> unterstützt @user und #PR-Tags
  - Senden: <button> text="Senden" (deaktiviert, bis das Eingabefeld Inhalt hat)

Phase 2: Proben

Führe alle Schritte ohne Aufzeichnung aus. Überprüfe, ob jeder Selektor aufgelöst werden kann.

Warum

Stille Selektorfehler sind der Hauptgrund, warum Demo-Aufnahmen fehlschlagen. Das Proben fängt sie ab, bevor eine Aufnahme verschwendet wird.

Wie

Verwende ensureVisible, einen Wrapper, der protokolliert und laut fehlschlägt:

async function ensureVisible(page, locator, label) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    const msg = `PROBEN FEHLGESCHLAGEN: "${label}" nicht gefunden - Selektor: ${typeof locator === 'string' ? locator : '(Locator-Objekt)'}`;
    console.error(msg);
    const found = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
        .filter(el => el.offsetParent !== null)
        .map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
        .join('\n  ');
    });
    console.error('  Sichtbare Elemente:\n  ' + found);
    return false;
  }
  console.log(`PROBEN OK: "${label}"`);
  return true;
}

Probenskriptstruktur

const steps = [
  { label: 'E-Mail-Feld anmelden', selector: '#email' },
  { label: 'Anmelden-Button', selector: 'button[type="submit"]' },
  { label: 'Neue Anfrage Schaltfläche', selector: 'button:has-text("Neue Anfrage")' },
  { label: 'Budget Code Select', selector: 'select' },
  { label: 'Lieferdatum', selector: 'input[type="date"]:visible' },
  { label: 'Beschreibungsfeld', selector: 'textarea:visible' },
  { label: 'Element hinzufügen Schaltfläche', selector: 'button:has-text("Element hinzufügen")' },
  { label: 'Absenden-Button', selector: 'button:has-text("Absenden")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('PROBEN FEHLGESCHLAGEN - Selektoren vor der Aufnahme korrigieren');
  process.exit(1);
}
console.log('PROBEN BESTANDEN - alle Selektoren verifiziert');

Wenn das Proben fehlschlägt

  1. Lies die Ausgabe der sichtbaren Elemente.
  2. Finde den korrekten Selektor.
  3. Aktualisiere das Skript.
  4. Führe das Proben erneut aus.
  5. Fahre nur fort, wenn jeder Selektor bestanden wird.

Phase 3: Aufnehmen

Erst nach erfolgreichem Entdecken und Proben sollte die Aufnahme erstellt werden.

Aufnahmeprinzipien

1. Erzählerische Struktur

Plane das Video als Geschichte. Befolge die vom Benutzer angegebene Reihenfolge oder verwende diese Standardreihenfolge:

  • Einstieg: Anmelden oder zum Startpunkt navigieren
  • Kontext: Überfliege die Umgebung, damit sich die Zuschauer orientieren können
  • Aktion: Führe die Hauptarbeitsschritte aus
  • Variation: Zeige eine sekundäre Funktion wie Einstellungen, Design oder Lokalisierung
  • Ergebnis: Zeige das Ergebnis, die Bestätigung oder den neuen Zustand

2. Tempo

  • Nach der Anmeldung: 4s
  • Nach der Navigation: 3s
  • Nach dem Klicken einer Schaltfläche: 2s
  • Zwischen größeren Schritten: 1,5-2s
  • Nach der letzten Aktion: 3s
  • Eingabeverzögerung: 25-40ms pro Zeichen

3. Cursor-Überlagerung

Füge einen SVG-Pfeil-Cursor ein, der den Mausbewegungen folgt:

async function injectCursor(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-cursor')) return;
    const cursor = document.createElement('div');
    cursor.id = 'demo-cursor';
    cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
    cursor.style.cssText = `
      position: fixed; z-index: 999999; pointer-events: none;
      width: 24px; height: 24px;
      transition: left 0.1s, top 0.1s;
      filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
    `;
    cursor.style.left = '0px';
    cursor.style.top = '0px';
    document.body.appendChild(cursor);
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });
  });
}

Rufe injectCursor(page) nach jeder Seitennavigation auf, da die Überlagerung bei Navigation zerstört wird.

4. Mausbewegung

Teleportiere den Cursor niemals. Bewege dich zum Ziel, bevor geklickt wird:

async function moveAndClick(page, locator, label, opts = {}) {
  const { postClickDelay = 800, ...clickOpts } = opts;
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`WARNUNG: moveAndClick übersprungen - "${label}" nicht sichtbar`);
    return false;
  }
  try {
    await el.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const box = await el.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
      await page.waitForTimeout(400);
    }
    await el.click(clickOpts);
  } catch (e) {
    console.error(`WARNUNG: moveAndClick fehlgeschlagen bei "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

Jeder Aufruf sollte ein beschreibendes label für die Fehlersuche enthalten.

5. Eingabe

Gib sichtbar ein, nicht sofort füllen:

async function typeSlowly(page, locator, text, label, charDelay = 35) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`WARNUNG: typeSlowly übersprungen - "${label}" nicht sichtbar`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. Scrollen

Verwende sanftes Scrollen anstelle von Sprüngen:

await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);

7. Dashboard-Panning

Wenn ein Dashboard oder eine Übersichtsseite gezeigt wird, bewege den Cursor über die wichtigsten Elemente:

async function panElements(page, selector, maxCount = 6) {
  const elements = await page.locator(selector).all();
  for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
    try {
      const box = await elements[i].boundingBox();
      if (box && box.y < 700) {
        await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
        await page.waitForTimeout(600);
      }
    } catch (e) {
      console.warn(`WARNUNG: panElements übersprungen Element ${i} (Selektor: "${selector}"): ${e.message}`);
    }
  }
}

8. Untertitel

Füge eine Untertitelzeile am unteren Rand des Viewports ein:

async function injectSubtitleBar(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-subtitle')) return;
    const bar = document.createElement('div');
    bar.id = 'demo-subtitle';
    bar.style.cssText = `
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
      text-align: center; padding: 12px 24px;
      background: rgba(0, 0, 0, 0.75);
      color: white; font-family: -apple-system, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
      transition: opacity 0.3s;
      pointer-events: none;
    `;
    bar.textContent = '';
    bar.style.opacity = '0';
    document.body.appendChild(bar);
  });
}

async function showSubtitle(page, text) {
  await page.evaluate((t) => {
    const bar = document.getElementById('demo-subtitle');
    if (!bar) return;
    if (t) {
      bar.textContent = t;
      bar.style.opacity = '1';
    } else {
      bar.style.opacity = '0';
    }
  }, text);
  if (text) await page.waitForTimeout(800);
}

Rufe injectSubtitleBar(page) zusammen mit injectCursor(page) nach jeder Navigation auf.

Verwendungsmuster:

await showSubtitle(page, 'Schritt 1 - Anmelden');
await showSubtitle(page, 'Schritt 2 - Dashboard-Übersicht');
await showSubtitle(page, '');

Richtlinien:

  • Halte den Untertitel kurz, idealerweise unter 60 Zeichen.
  • Verwende das Format Schritt N - Aktion für Konsistenz.
  • Lösche den Untertitel während längerer Pausen, in denen die UI für sich selbst sprechen kann.

Skriptvorlage

'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-FEATURE.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// Füge hier injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible und panElements ein.

(async () => {
  const browser = await chromium.launch({ headless: true });

  if (REHEARSAL) {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    // Navigiere durch den Workflow und führe ensureVisible für jeden Selektor aus.
    await browser.close();
    return;
  }

  const context = await browser.newContext({
    recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();

  try {
    await injectCursor(page);
    await injectSubtitleBar(page);

    await showSubtitle(page, 'Schritt 1 - Anmelden');
    // Anmeldeaktionen

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, 'Schritt 2 - Dashboard-Übersicht');
    // Dashboard-Panning

    await showSubtitle(page, 'Schritt 3 - Hauptworkflow');
    // Aktionssequenz

    await showSubtitle(page, 'Schritt 4 - Ergebnis');
    // Endgültige Enthüllung
    await showSubtitle(page, '');
  } catch (err) {
    console.error('DEMO-FEHLER:', err.message);
  } finally {
    await context.close();
    const video = page.video();
    if (video) {
      const src = await video.path();
      const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
      try {
        fs.copyFileSync(src, dest);
        console.log('Video gespeichert:', dest);
      } catch (e) {
        console.error('FEHLER: Video konnte nicht kopiert werden:', e.message);
        console.error('  Quelle:', src);
        console.error('  Ziel:', dest);
      }
    }
    await browser.close();
  }
})();

Verwendung:

# Phase 2: Proben
node demo-script.cjs --rehearse

# Phase 3: Aufnehmen
node demo-script.cjs

Checkliste vor der Aufnahme

  • Entdeckungsphase abgeschlossen
  • Proben bestanden, alle Selektoren OK
  • Headless-Modus aktiviert
  • Auflösung auf 1280x720 eingestellt
  • Cursor- und Untertitelüberlagerungen nach jeder Navigation neu eingefügt
  • showSubtitle(page, 'Schritt N - ...') an wichtigen Übergängen verwendet
  • moveAndClick für alle Klicks mit beschreibenden Labels verwendet
  • typeSlowly für sichtbare Eingaben verwendet
  • Keine stillen Abfangmechanismen;

… [für Übersetzung abgeschnitten]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Verwandte Skills