Skip to content

immense/playwright-readme-videos

Repository files navigation

playwright-readme-videos

Automatically convert Playwright test videos to MP4 or GIF and embed them in your README.

Why?

When you record Playwright tests, they're saved as .webm files which:

  • May not play in all environments
  • Aren't automatically embedded in documentation
  • Need manual conversion and README updates

This package automates the entire workflow: convert videos → copy to assets folder → update README with proper tags.

Features

  • 🎬 Convert Playwright .webm videos to MP4 or GIF
  • 📝 Automatically update README with video tags
  • 🎯 Select specific tests via glob patterns
  • 🔧 Fully configurable (format, directories, markers, etc.)
  • 📦 Works as CLI tool or programmatic API
  • 🚀 Zero config needed for common use cases

Installation

npm install --save-dev playwright-readme-videos

Quick Start

1. Add markers to your README

# My Project

## Demo

<!-- pw-videos:start -->
<!-- pw-videos:end -->

2. Run your Playwright tests

playwright test

3. Process the videos

npx pw-test-videos

That's it! Your README now includes the test video.

Usage

CLI

# Basic usage (MP4, newest video)
npx pw-test-videos

# Convert to GIF
npx pw-test-videos --format gif --name demo.gif

# Process multiple videos
npx pw-test-videos --max 3

# Custom directories
npx pw-test-videos --from test-results --to assets/videos

# Full example
npx pw-test-videos \
  --format mp4 \
  --from test-results \
  --to docs/videos \
  --readme README.md \
  --max 1 \
  --name demo.mp4

Programmatic API

import { processTestVideos } from 'playwright-readme-videos';

await processTestVideos({
  format: 'mp4',
  sourceDir: 'test-results',
  outputDir: 'docs/pw-videos',
  readmePath: 'README.md',
  maxVideos: 1,
  outputFilename: 'demo.mp4',
});

npm Scripts

Add to your package.json:

{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:mp4": "playwright test && pw-test-videos",
    "test:e2e:gif": "playwright test && pw-test-videos --format gif --name demo.gif"
  }
}

GitHub Actions

This repo exposes:

  • A composite action (for Marketplace later, and for local path usage today): action.yml
  • A reusable workflow: .github/workflows/ci.yml (triggerable via workflow_call)
  • A CI helper action for running this repo’s tests when consumed as a submodule: .github/actions/ci/action.yml

Consume as a local action (e.g. via git submodule)

In another repo (like vscode-copilot-debugger) with this repo checked out under external/playwright-test-videos, you can run the submodule tests like this:

- name: Test playwright-test-videos
  uses: ./external/playwright-test-videos/.github/actions/ci
  with:
    working-directory: external/playwright-test-videos
    # Note: set cache: 'false' under act / local runners if caching hangs.
    cache: 'true'
    run: npm test

Consume the reusable workflow (future / published usage)

Once this repo is referenced by a git ref (tag/sha), another repo can call the reusable workflow:

jobs:
  pw-test-videos:
    uses: dkattan/playwright-test-videos/.github/workflows/ci.yml@v0
    with:
      run: npm run test:ci

For now (when vendored as a submodule), prefer the local composite action shown above.

Configuration

The package supports three configuration methods with the following priority: CLI arguments > Environment variables > Config file > Defaults

Config File (Recommended)

Create a config file in your project root. The package will auto-discover it:

Supported file names:

  • pw-test-videos.config.js or .mjs (JavaScript/ESM)
  • pw-test-videos.config.json (JSON)
  • .pw-test-videosrc or .pw-test-videosrc.json (JSON)

JavaScript example:

// pw-test-videos.config.js
export default {
  format: 'mp4',
  sourceDir: 'test-results',
  outputDir: 'docs/pw-videos',
  readmePath: 'README.md',
  testPattern: '**/*.webm',
  maxVideos: 1,
  outputFilename: 'demo.mp4',
  startMarker: '<!-- pw-videos:start -->',
  endMarker: '<!-- pw-videos:end -->',
  overwrite: false,
};

JSON example:

{
  "format": "gif",
  "maxVideos": 3,
  "outputDir": "assets/videos"
}

You can also specify a custom config file:

npx pw-test-videos --config my-config.json

Environment Variables

Prefix all config options with PW_TEST_VIDEOS_:

# Set format to GIF
export PW_TEST_VIDEOS_FORMAT=gif

# Set output directory
export PW_TEST_VIDEOS_OUTPUT_DIR=assets/videos

# Set max videos
export PW_TEST_VIDEOS_MAX_VIDEOS=3

# Run the tool
npx pw-test-videos

Available environment variables:

  • PW_TEST_VIDEOS_FORMAT - Video format (mp4 or gif)
  • PW_TEST_VIDEOS_SOURCE_DIR - Source directory
  • PW_TEST_VIDEOS_OUTPUT_DIR - Output directory
  • PW_TEST_VIDEOS_README_PATH - README path
  • PW_TEST_VIDEOS_TEST_PATTERN - Glob pattern
  • PW_TEST_VIDEOS_MAX_VIDEOS - Max videos
  • PW_TEST_VIDEOS_OUTPUT_FILENAME - Output filename
  • PW_TEST_VIDEOS_START_MARKER - Start marker
  • PW_TEST_VIDEOS_END_MARKER - End marker
  • PW_TEST_VIDEOS_OVERWRITE - Overwrite (true/false)
  • PW_TEST_VIDEOS_DEDUPE - De-dupe frames (true/false)
  • PW_TEST_VIDEOS_DEDUPE_START_AFTER_SECONDS - Only start de-duping after N seconds (e.g. 0.5)
  • PW_TEST_VIDEOS_DEDUPE_END_PAD_SECONDS - Ensure the final view is visible for at least N seconds (e.g. 0.5)
  • PW_TEST_VIDEOS_REMOVE_RANGES - Remove ranges JSON array
  • PW_TEST_VIDEOS_SIDECAR_MODE - Sidecar mode (none or adjacent)
  • PW_TEST_VIDEOS_SIDECAR_SUFFIX - Sidecar suffix (e.g. .ranges.json)
  • PW_TEST_VIDEOS_SIDECAR_REQUIRED - Sidecar required (true/false)

CLI Arguments

CLI arguments override all other configuration sources:

npx pw-test-videos --format gif --max 3 --to assets/videos

Configuration

PlaywrightTestVideosConfig

interface PlaywrightTestVideosConfig {
  /** Video format: 'mp4' or 'gif' (default: 'mp4') */
  format?: 'mp4' | 'gif';

  /** Source directory with test videos (default: 'test-results') */
  sourceDir?: string;

  /** Output directory for processed videos (default: 'docs/pw-videos') */
  outputDir?: string;

  /** README file path (default: 'README.md') */
  readmePath?: string;

  /** Glob pattern(s) to select videos (default: '**/*.webm') */
  testPattern?: string | string[];

  /** Max number of videos to include (default: 1) */
  maxVideos?: number;

  /** Output filename when maxVideos=1 (default: 'demo.mp4' or 'demo.gif') */
  outputFilename?: string;

  /** Start marker in README (default: '<!-- pw-videos:start -->') */
  startMarker?: string;

  /** End marker in README (default: '<!-- pw-videos:end -->') */
  endMarker?: string;

  /** Overwrite existing converted videos (default: false) */
  overwrite?: boolean;

  /**
   * Remove duplicate/near-duplicate frames to shorten long pauses.
   *
   * Uses ffmpeg's mpdecimate filter during conversion.
   * Note: animated spinners prevent de-dupe because frames are no longer identical.
   * In that case, prefer `removeRanges` (or sidecar ranges) to cut waiting windows by time.
   */
  dedupe?: boolean | {
    /** Only start de-duping after this many seconds from the start of the (trimmed) video. Default: 0.5 */
    startAfterSeconds?: number;
    /** Ensure the final view is visible for at least this many seconds by padding the last frame. Default: 0.5 */
    endPadSeconds?: number;
  };

  /**
   * Optional time ranges to remove from the output video (seconds).
   * Useful when you can emit timestamps from your tests (e.g. spinner visible/hidden)
   * and want to cut waiting sections purely by time.
   */
  removeRanges?: Array<{ startSeconds: number; endSeconds: number; keepStartSeconds?: number; keepEndSeconds?: number }>;

  /**
   * Optional sidecar file mode for per-video ranges.
   * - 'none' (default): do not read sidecars.
   * - 'adjacent': look for '<videoPath><sidecarSuffix>' next to each input video.
   */
  sidecarMode?: 'none' | 'adjacent';

  /** Suffix for adjacent sidecar files (default: '.ranges.json') */
  sidecarSuffix?: string;

  /** If true, fail if an expected adjacent sidecar is missing (default: false) */
  sidecarRequired?: boolean;
}

Shortening “waiting” videos

Playwright videos often contain long waits (build/startup/network). Those waits usually produce many identical frames that can be safely removed.

There are two complementary strategies:

1) Trim by time ranges (recommended)

If your tests (or a post-processor) can emit timestamps for “waiting windows” (e.g. spinner visible → spinner hidden), you can remove those windows using removeRanges.

Programmatic example:

import { convertVideo } from 'playwright-test-videos';

convertVideo('path/to/video.webm', {
  format: 'mp4',
  overwrite: true,
  removeRanges: [
    { startSeconds: 3.1, endSeconds: 8.7, keepStartSeconds: 0.35, keepEndSeconds: 0.35 },
  ],
});

2) Use adjacent sidecar JSON ranges (recommended for CI)

When sidecarMode: 'adjacent', the tool will look for a JSON file next to each input video (default suffix: .ranges.json).

This lets your tests write per-video trimming metadata without having to plumb it through config globals.

Optional: frame de-duplication (dedupe)

You can also enable dedupe: true to collapse long stretches of identical frames using ffmpeg’s mpdecimate.

By default, de-dupe only starts after the first 0.5 seconds so the initial UI state isn’t “collapsed away”. You can customize this with:

  • dedupe: { startAfterSeconds: 0 } (de-dupe from the beginning)
  • dedupe: { startAfterSeconds: 0.5 } (default)
  • dedupe: { startAfterSeconds: 1.2 } (leave more context)

Also by default, the final UI state is padded (last frame cloned) to be visible for 0.5 seconds:

  • dedupe: { endPadSeconds: 0 } (no padding; final state may display for only a single frame)
  • dedupe: { endPadSeconds: 0.5 } (default)

If the UI contains an animated spinner, frames won’t be identical, so time-based trimming (above) is usually the better fit.

Demo (repo-local)

In this repository, there’s a small end-to-end demo script that records a spinner page, then produces:

  • unshortened.mp4 (baseline)
  • shortened.mp4 (spinner window removed via adjacent sidecar ranges)
  • shortened-dedupe.mp4 (optional “extra short”: trimming + dedupe: true)

It writes artifacts under:

  • external/playwright-test-videos/test-artifacts/spinner-demo/**

Run:

  • npm --prefix external/playwright-test-videos run demo:spinner

If you need Playwright browsers installed:

  • npm --prefix external/playwright-test-videos run demo:spinner:ci

CLI Options

--format <mp4|gif>       Video format (default: mp4)
--from <dir>             Source directory (default: test-results)
--to <dir>               Output directory (default: docs/pw-videos)
--readme <path>          README file (default: README.md)
--pattern <glob>         Glob pattern (default: **/*.webm)
--max <n>                Max videos to process (default: 1)
--name <filename>        Output filename when max=1
--start-marker <text>    Start marker in README
--end-marker <text>      End marker in README
--overwrite              Overwrite existing videos
--dedupe                 Remove duplicate/near-duplicate frames to shorten pauses
--dedupe-start-after <s> Only start de-duping after this many seconds (default: 0.5 when dedupe enabled)
--dedupe-start-after-ms <ms> Same as --dedupe-start-after, but in milliseconds
--dedupe-end-pad <s>     Ensure the final view is visible for at least this many seconds (default: 0.5 when dedupe enabled)
--dedupe-end-pad-ms <ms> Same as --dedupe-end-pad, but in milliseconds
--remove-range <s,e>     Remove a time range in seconds (can be specified multiple times)
--remove-ranges <json>   Remove ranges as JSON array (e.g. '[{"startSeconds":1,"endSeconds":2}]')
--sidecar-adjacent       Read per-video sidecar JSON ranges next to each input video
--sidecar-suffix <text>  Sidecar suffix when using --sidecar-adjacent (default: .ranges.json)
--sidecar-required       Fail if an expected sidecar file is missing
-h, --help               Show help

Examples

Single Test Demo

# Run a specific test and convert to MP4
playwright test -g "login flow"
npx pw-test-videos --name login-demo.mp4

Multiple Test Videos

# Process up to 3 newest test videos
npx pw-test-videos --max 3

GIF for GitHub

# Convert to GIF (better for GitHub README previews)
npx pw-test-videos --format gif --name demo.gif

Custom Glob Pattern

# Only process videos from "smoke" tests
npx pw-test-videos --pattern "**/smoke-*.webm"

Custom README Markers

# Use different markers
npx pw-test-videos \
  --start-marker "<!-- videos:start -->" \
  --end-marker "<!-- videos:end -->"

How It Works

  1. Find Videos: Searches for .webm files in source directory using glob pattern
  2. Sort: Orders by modification time (newest first)
  3. Select: Takes up to maxVideos based on configuration
  4. Convert: Uses ffmpeg to convert to MP4 or GIF
    • MP4: H.264 codec for broad compatibility
    • GIF: Two-pass conversion with palette generation for quality
  5. Copy: Moves converted videos to output directory
  6. Update: Replaces content between markers in README with video tags

Requirements

ffmpeg

This package requires ffmpeg to be installed on your system.

macOS:

brew install ffmpeg

Ubuntu/Debian:

sudo apt-get install ffmpeg

Windows: Download from ffmpeg.org or use chocolatey:

choco install ffmpeg

Playwright

Your project should have Playwright configured with video recording:

// playwright.config.ts
export default defineConfig({
  use: {
    video: 'on', // or 'retain-on-failure'
  },
});

MP4 vs GIF

MP4

✅ Smaller file size ✅ Better quality ✅ Supports audio (though test videos typically have none) ❌ May not autoplay in all contexts

GIF

✅ Autoplays everywhere ✅ No player controls needed ❌ Larger file size ❌ Limited color palette ❌ Lower quality

Recommendation: Use MP4 by default, GIF for specific use cases where autoplay is essential.

Integration Examples

With GitHub Actions

You can also use this package as a reusable GitHub Action.

Because this action lives in a subdirectory, reference it with the full path:

jobs:
  videos:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Run your tests first (this action only converts/embeds videos)
      - name: Run Playwright tests
        run: npx playwright test

      - name: Convert videos + update README
        uses: immense/playwright-readme-videos@<ref>
        with:
          # Directory containing your Playwright test-results/ and README markers
          working-directory: .
          # Optional: override any of the CLI args below
          format: mp4
          from: test-results
          to: docs/pw-videos
          readme: README.md
          max: "1"
          name: demo.mp4

      # Optional: commit/push the updated README + assets
      - name: Commit updated README/videos
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add README.md docs/
          git commit -m "Update Playwright demo videos" || echo "No changes"
          git push

Action inputs

The action inputs map to the CLI flags (when provided):

  • config--config
  • format--format
  • from--from
  • to--to
  • readme--readme
  • pattern--pattern
  • max--max
  • name--name
  • start-marker--start-marker
  • end-marker--end-marker
  • overwrite--overwrite (set to true)

The action will install ffmpeg on the runner by default (set install-ffmpeg: "false" to skip).

- name: Run Playwright tests
  run: npm run test:e2e

- name: Process test videos
  run: npx pw-test-videos

- name: Commit updated README
  run: |
    git config user.name github-actions
    git config user.email github-actions@github.com
    git add README.md docs/
    git commit -m "Update test videos" || echo "No changes"
    git push

With npm-run-all

{
  "scripts": {
    "test:e2e": "playwright test",
    "videos:convert": "pw-test-videos",
    "test:e2e:demo": "run-s test:e2e videos:convert"
  }
}

Troubleshooting

"ffmpeg was not found"

Install ffmpeg (see Requirements section).

"README is missing token block"

Ensure your README has both start and end markers.

"No videos found"

Check that:

  • Playwright tests have run
  • Video recording is enabled in playwright.config.ts
  • sourceDir path is correct

Videos are too large

  • Use MP4 instead of GIF
  • Reduce video dimensions in Playwright config
  • Lower frame rate for GIFs

API Reference

processTestVideos(config?)

Main function to process test videos.

Parameters:

  • config: Optional configuration object

Returns: Promise<ProcessResult>

interface ProcessResult {
  converted: ConversionResult[];
  copied: string[];
}

Example:

const result = await processTestVideos({
  format: 'gif',
  maxVideos: 2,
});
console.log(`Converted ${result.converted.length} videos`);

License

MIT

Contributing

Contributions welcome! Please feel free to submit a Pull Request.

Credits

Originally developed as part of the relative-link-handler VS Code extension project.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published