Communitygithub.com

ui-demo

Enregistrez des vidéos de démonstration d'interface utilisateur soignées à l'aide de Playwright. Utilisez-le lorsque l'utilisateur demande la création d'une démo, d'une visite guidée, d'un enregistrement d'écran ou d'un didacticiel vidéo d'une application web. Produit des vidéos WebM avec un curseur visible, un rythme naturel et une sensation professionnelle.

Qu'est-ce que ui-demo ?

Cette compétence d'agent IA permet de créer des vidéos de démonstration d'interface utilisateur professionnelles à l'aide de Playwright. Elle est conçue pour être utilisée lorsque l'utilisateur demande une démo vidéo, un enregistrement d'écran, une visite guidée ou un tutoriel d'une application web. La compétence suit un processus en trois phases : Découverte, Répétition et Enregistrement. La phase de Découverte explore les pages cibles pour comprendre les éléments interactifs réels et éviter les hypothèses qui cassent silencieusement les enregistrements. La phase de Répétition vérifie que tous les sélecteurs de la scénario fonctionnent correctement, pour éviter les échecs silencieux d'enregistrement. La phase d'Enregistrement produit une vidéo WebM de haute qualité avec un curseur SVG superposé qui suit les mouvements de la souris, un rythme naturel avec des délais prédéfinis, une barre de sous-titres intégrée, des défilements fluides, et une saisie de texte visible caractère par caractère. La compétence propose des fonctions réutilisables pour le déplacement et le clic, la frappe lente, le panoramique de tableau de bord, et les sous-titres, ainsi qu'un modèle de script complet avec des options pour la répétition et l'enregistrement. Elle est idéale pour créer des vidéos de démonstration à intégrer dans la documentation, l'intégration des nouveaux utilisateurs, ou les présentations aux parties prenantes.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Enregistreur de Vidéos de Démonstration UI

Enregistrez des vidéos de démonstration soignées d'applications web en utilisant l'enregistrement vidéo de Playwright avec un curseur superposé injecté, un rythme naturel et un flux narratif.

Quand l'utiliser

  • L'utilisateur demande une « vidéo de démo », un « enregistrement d'écran », une « visite guidée » ou un « tutoriel »
  • L'utilisateur souhaite présenter visuellement une fonctionnalité ou un flux de travail
  • L'utilisateur a besoin d'une vidéo pour la documentation, l'intégration ou une présentation aux parties prenantes

Processus en Trois Phases

Chaque démo passe par trois phases : Découvrir -> Répéter -> Enregistrer. Ne sautez jamais directement à l'enregistrement.


Phase 1 : Découvrir

Avant d'écrire un scénario, explorez les pages cibles pour comprendre ce qui s'y trouve réellement.

Pourquoi

Vous ne pouvez pas scénariser ce que vous n'avez pas vu. Les champs peuvent être <input> et non <textarea>, les listes déroulantes peuvent être des composants personnalisés et non <select>, et les zones de commentaires peuvent prendre en charge les @mentions ou les #tags. Les hypothèses cassent les enregistrements silencieusement.

Comment

Naviguez vers chaque page du flux et videz ses éléments interactifs :

// Exécutez ceci pour chaque page du flux AVANT d'écrire le script de démo
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));

Ce qu'il faut rechercher

  • Champs de formulaire : Sont-ils <select>, <input>, des listes déroulantes personnalisées ou des combobox ?
  • Options de sélection : Videz les valeurs ET le texte des options. Les espaces réservés ont souvent value="0" ou value="" qui semble non vide. Utilisez Array.from(el.options).map(o => ({ value: o.value, text: o.text })). Ignorez les options dont le texte inclut « Select » ou dont la valeur est "0".
  • Texte enrichi : La zone de commentaire prend-elle en charge les @mentions, les #tags, le markdown ou les emoji ? Vérifiez le texte de l'espace réservé.
  • Champs obligatoires : Quels champs bloquent la soumission du formulaire ? Vérifiez required, * dans les étiquettes, et essayez de soumettre vide pour voir les erreurs de validation.
  • Contenu dynamique : Les champs apparaissent-ils après avoir rempli d'autres champs ?
  • Étiquettes des boutons : Texte exact comme "Submit", "Submit Request" ou "Send".
  • En-têtes de colonnes de tableau : Pour les modales pilotées par tableau, mappez chaque input[type="number"] à son en-tête de colonne au lieu de supposer que toutes les entrées numériques signifient la même chose.

Résultat

Un mappage de champs pour chaque page, utilisé pour écrire des sélecteurs corrects dans le script. Exemple :

/purchase-requests/new:
  - Budget Code: <select> (premier select sur la page, 4 options)
  - Desired Delivery: <input type="date">
  - Context: <textarea> (pas input)
  - BOM table: cellules éditables en ligne avec span.cursor-pointer -> pattern input
  - Submit: <button> text="Submit"

/purchase-requests/N (detail):
  - Comment: <input placeholder="Type a message..."> prend en charge @user et #PR tags
  - Send: <button> text="Send" (désactivé tant que l'entrée n'a pas de contenu)

Phase 2 : Répéter

Parcourez toutes les étapes sans enregistrement. Vérifiez que chaque sélecteur se résout.

Pourquoi

Les échecs silencieux de sélecteurs sont la principale raison pour laquelle les enregistrements de démo se cassent. La répétition les attrape avant de gaspiller un enregistrement.

Comment

Utilisez ensureVisible, un wrapper qui journalise et échoue bruyamment :

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 = `REPETITION ÉCHEC : "${label}" introuvable - sélecteur : ${typeof locator === 'string' ? locator : '(objet locator)'}`;
    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('  Éléments visibles :\n  ' + found);
    return false;
  }
  console.log(`REPETITION OK : "${label}"`);
  return true;
}

Structure du script de répétition

const steps = [
  { label: 'Champ email de connexion', selector: '#email' },
  { label: 'Soumettre connexion', selector: 'button[type="submit"]' },
  { label: 'Bouton Nouvelle demande', selector: 'button:has-text("New Request")' },
  { label: 'Select code budgétaire', selector: 'select' },
  { label: 'Date de livraison', selector: 'input[type="date"]:visible' },
  { label: 'Champ description', selector: 'textarea:visible' },
  { label: 'Bouton Ajouter un article', selector: 'button:has-text("Add Item")' },
  { label: 'Bouton Soumettre', selector: 'button:has-text("Submit")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('REPETITION ÉCHEC - corrigez les sélecteurs avant d\'enregistrer');
  process.exit(1);
}
console.log('REPETITION RÉUSSIE - tous les sélecteurs vérifiés');

Quand la répétition échoue

  1. Lisez le vidage des éléments visibles.
  2. Trouvez le sélecteur correct.
  3. Mettez à jour le script.
  4. Relancez la répétition.
  5. Ne continuez que lorsque chaque sélecteur réussit.

Phase 3 : Enregistrer

Créez l'enregistrement uniquement après que la découverte et la répétition ont réussi.

Principes d'enregistrement

1. Flux narratif

Planifiez la vidéo comme une histoire. Suivez l'ordre spécifié par l'utilisateur, ou utilisez celui par défaut :

  • Entrée : Connexion ou navigation vers le point de départ
  • Contexte : Parcourez les alentours pour que les spectateurs s'orientent
  • Action : Effectuez les étapes principales du flux de travail
  • Variation : Montrez une fonctionnalité secondaire comme les paramètres, le thème ou la localisation
  • Résultat : Montrez le résultat, la confirmation ou le nouvel état

2. Rythme

  • Après connexion : 4s
  • Après navigation : 3s
  • Après avoir cliqué sur un bouton : 2s
  • Entre les étapes majeures : 1.5-2s
  • Après l'action finale : 3s
  • Délai de frappe : 25-40ms par caractère

3. Superposition de curseur

Injectez un curseur fléché SVG qui suit les mouvements de la souris :

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';
    });
  });
}

Appelez injectCursor(page) après chaque navigation de page car la superposition est détruite lors de la navigation.

4. Mouvement de la souris

Ne téléportez jamais le curseur. Déplacez-vous vers la cible avant de cliquer :

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(`ATTENTION : moveAndClick ignoré - "${label}" non visible`);
    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(`ATTENTION : moveAndClick a échoué sur "${label}" : ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

Chaque appel doit inclure un label descriptif pour le débogage.

5. Saisie

Saisissez visiblement, pas un remplissage instantané :

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(`ATTENTION : typeSlowly ignoré - "${label}" non visible`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. Défilement

Utilisez un défilement fluide au lieu de sauts :

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

7. Panoramique de tableau de bord

Lors de l'affichage d'un tableau de bord ou d'une page de présentation, déplacez le curseur sur les éléments clés :

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(`ATTENTION : panElements a ignoré l'élément ${i} (sélecteur : "${selector}") : ${e.message}`);
    }
  }
}

8. Sous-titres

Injectez une barre de sous-titres en bas de la fenêtre :

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);
}

Appelez injectSubtitleBar(page) en même temps que injectCursor(page) après chaque navigation.

Modèle d'utilisation :

await showSubtitle(page, 'Étape 1 - Connexion');
await showSubtitle(page, 'Étape 2 - Aperçu du tableau de bord');
await showSubtitle(page, '');

Directives :

  • Gardez le texte des sous-titres court, idéalement en dessous de 60 caractères.
  • Utilisez le format Étape N - Action pour la cohérence.
  • Effacez le sous-titre lors de longues pauses où l'interface peut parler d'elle-même.

Modèle de script

'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');

// Collez injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible et panElements ici.

(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();
    // Parcourez le flux et exécutez ensureVisible pour chaque sélecteur.
    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, 'Étape 1 - Connexion');
    // actions de connexion

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, 'Étape 2 - Aperçu du tableau de bord');
    // panoramique du tableau de bord

    await showSubtitle(page, 'Étape 3 - Flux de travail principal');
    // séquence d'actions

    await showSubtitle(page, 'Étape 4 - Résultat');
    // révélation finale
    await showSubtitle(page, '');
  } catch (err) {
    console.error('ERREUR DE DÉMO :', 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('Vidéo enregistrée :', dest);
      } catch (e) {
        console.error('ERREUR : Échec de la copie de la vidéo :', e.message);
        console.error('  Source :', src);
        console.error('  Destination :', dest);
      }
    }
    await browser.close();
  }
})();

Utilisation :

# Phase 2 : Répéter
node demo-script.cjs --rehearse

# Phase 3 : Enregistrer
node demo-script.cjs

Liste de vérification avant l'enregistrement

  • Phase de découverte terminée
  • La répétition réussit avec tous les sélecteurs OK
  • Mode headless activé
  • Résolution réglée sur 1280x720
  • Superpositions de curseur et de sous-titres réinjectées après chaque navigation
  • showSubtitle(page, 'Étape N - ...') utilisé aux transitions majeures
  • moveAndClick utilisé pour tous les clics avec des étiquettes descriptives
  • typeSlowly utilisé pour la saisie visible
  • Pas de captures silencieuses ; toutes les erreurs sont journalisées
  • Le nom de sortie de la vidéo correspond à la fonctionnalité
  • La description du sous-titre a été supprimée avant la fin de la vidéo

Individual skills in this repo

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

Skills associés