ui-screenshots

Par github · awesome-copilot

Capturez des captures d'écran d'applications web pendant le développement avec Playwright et PIL. Prend en charge les captures pleine page, les états interactifs et un workflow itératif de recadrage qui évite les re-captures lentes.

npx skills add https://github.com/github/awesome-copilot --skill ui-screenshots

Captures d'écran UI

Capturez des captures d'écran d'applications web et d'interfaces graphiques pendant le développement pour documenter les changements visuels.

Quand utiliser cette skill

Utilisez cette skill quand vous devez :

  • Capturer l'état actuel d'une application web en cours d'exécution
  • Documenter une interface avant et après une modification de code
  • Capturer des états interactifs (tooltips, survols, éléments sélectionnés)
  • Capturer des sections spécifiques d'une page sans refaire de capture

Prérequis

pip install playwright Pillow -q
playwright install chromium

Workflow principal

1. Prendre une capture brute de page complète

from playwright.async_api import async_playwright

async def capture(url="http://localhost:3000", out="screenshot-raw.png", width=1400, height=5000):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page(viewport={"width": width, "height": height})
        await page.goto(url, wait_until="networkidle")
        await page.wait_for_timeout(4000)  # let charts/animations render
        await page.screenshot(path=out, full_page=True)
        await browser.close()
  • Utilisez une fenêtre d'affichage haute (height=5000) pour que la page affiche tout sans défilement
  • wait_until="networkidle" + wait_for_timeout(4000) garantit que les graphiques asynchrones se chargent
  • full_page=True capture tout le contenu déroulable

2. Regarder l'image brute, puis rogner avec PIL

N'essayez PAS d'obtenir des rognages parfaits via le paramètre clip de Playwright. C'est peu fiable avec les captures de page complète.

from PIL import Image

img = Image.open("screenshot-raw.png")
cropped = img.crop((left, top, right, bottom))  # adjust based on what you see
cropped.save("screenshot-final.png")
  1. Prenez la capture brute
  2. Regardez-la pour voir les positions réelles en pixels
  3. Rognez avec PIL en fonction de ce que vous voyez
  4. Regardez le résultat — si ce n'est pas correct, re-rognez (instantané, pas besoin de nouvelle capture)

3. Itérer sur le rognage, pas sur la capture

  • Re-capturer est lent (lancement du navigateur + chargement de la page + attente de rendu)
  • Re-rogner est instantané (juste PIL)
  • Obtenez une bonne capture brute, puis découpez-la de autant de façons que nécessaire

4. États interactifs

element = page.locator("selector").first
await element.hover()
await page.wait_for_timeout(1000)  # let tooltip appear
await page.screenshot(path="screenshot-hover.png", full_page=True)

Pour l'état « sélectionné » sans effet de survol, éloignez la souris après le clic :

await element.click()
await page.mouse.move(300, 300)  # move away so hover doesn't show
await page.wait_for_timeout(500)
await page.screenshot(path="screenshot-selected.png", full_page=True)

5. Captures spécifiques à une section

Rognez différentes sections d'une capture de page complète unique :

img.crop((0, 200, 920, 900)).save("screenshot-header.png")
img.crop((0, 900, 920, 1600)).save("screenshot-main.png")

Directives

  1. Toujours capturer l'état avant AVANT de faire des modifications — si vous oubliez, vous devez revenir en arrière le code pour obtenir une capture avant
  2. Les paires avant/après doivent utiliser la même largeur de fenêtre d'affichage et rognage — sinon la comparaison est inutile
  3. Pour obtenir un « avant » après avoir déjà modifié le code : utilisez git checkout HEAD~1 -- <files> pour revenir en arrière, prenez une capture, puis git checkout HEAD -- <files> pour restaurer
  4. Pour les états interactifs : capturez avant ET après pour chaque état — ne supposez pas que l'avant « normal » couvre tous les cas
  5. Utilisez device_scale_factor=1 dans Playwright pour forcer les pixels 1x afin que les captures correspondent à ce que les utilisateurs voient au zoom 100 %
  6. Les graphiques nécessitent un temps d'attente supplémentaire — Plotly, D3, etc. se rendent de manière asynchrone ; 4s minimum après networkidle
  7. Une fenêtre d'affichage étroite révèle les bugs de rendu — certains problèmes de bordure/alignement n'apparaissent qu'à des largeurs spécifiques

Captures d'écran d'applications non web

Pour les applications de bureau (VS, WPF, WinForms, applications console, terminaux) où Playwright ne peut pas accéder.

mss + ctypes (recommandé pour les fenêtres de bureau)

Trouvez une fenêtre par titre via l'API Win32, capturez sa région avec mss. Testé à ~33ms par capture.

import ctypes
from ctypes import c_int, Structure, byref, windll
import mss
from PIL import Image

user32 = windll.user32

def find_window(title_contains):
    """Find visible windows matching a title substring."""
    results = []
    WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
    def cb(hwnd, _):
        if user32.IsWindowVisible(hwnd):
            buf = ctypes.create_unicode_buffer(256)
            user32.GetWindowTextW(hwnd, buf, 256)
            if title_contains.lower() in buf.value.lower():
                results.append((hwnd, buf.value))
        return True
    user32.EnumWindows(WNDENUMPROC(cb), 0)
    return results

def capture_window(title_contains, output_path):
    """Capture a window by title substring."""
    windows = find_window(title_contains)
    if not windows:
        raise ValueError(f"No window matching '{title_contains}'")
    hwnd = windows[0][0]

    class RECT(Structure):
        _fields_ = [('left', c_int), ('top', c_int), ('right', c_int), ('bottom', c_int)]
    rect = RECT()
    user32.GetWindowRect(hwnd, byref(rect))
    w, h = rect.right - rect.left, rect.bottom - rect.top

    with mss.mss() as sct:
        shot = sct.grab({'left': rect.left, 'top': rect.top, 'width': w, 'height': h})
        img = Image.frombytes('RGB', shot.size, shot.rgb)
        img.save(output_path)
        return img

# Usage:
capture_window('Visual Studio Code', 'vscode-capture.png')

Prérequis : pip install mss pillow Limitation : La fenêtre doit être visible (pas cachée par d'autres fenêtres ou minimisée).

Applications Electron (VS Code, etc.)

Node.js Playwright seulement — Python Playwright n'a pas d'API electron. Les captures se font via CDP (Chrome DevTools Protocol), pas depuis l'écran — fonctionne même si minimisée.

const { _electron: electron } = require('playwright');
const app = await electron.launch({
    executablePath: 'C:\\Program Files\\Microsoft VS Code\\Code.exe',
    args: ['--new-window', '--disable-extensions', '--user-data-dir=' + tmpDir]
});
const window = await app.firstWindow();
await window.waitForLoadState('domcontentloaded');

// Minimize immediately — captures still work via CDP
await app.evaluate(({ BrowserWindow }) => {
    BrowserWindow.getAllWindows()[0].minimize();
});

await window.screenshot({ path: 'capture.png' }); // works while minimized!
await app.close();

Critique : --user-data-dir=<temp> est requis sinon VS Code délègue à l'instance existante et le processus lancé se termine immédiatement.

Arbre de décision

Scénario Outil Notes
Application web (localhost) Playwright Éprouvé, accès DOM complet
Application Electron (VS Code) Playwright Electron (Node.js) Fonctionne minimisée via CDP
Application de bureau, fenêtre visible mss + ctypes (trouver par titre) ~33ms par capture
Application de bureau, derrière des fenêtres Windows Graphics Capture API Configuration complexe, Win10 1903+
Plein écran rapide mss ~68ms

Limitations

  • La capture web nécessite une application s'exécutant localement ou une URL accessible
  • La capture de bureau (mss) nécessite que la fenêtre soit visible et sans obstruction
  • La capture Electron nécessite Node.js Playwright (pas Python)
  • Certaines SPA avec un rendu lourd côté client peuvent avoir besoin d'une logique d'attente personnalisée au-delà de networkidle

Skills similaires