Automated, reproducible demo videos for Phoenix / LiveView apps.
A demo is an ExUnit test built on PhoenixTest.Playwright.Case, so it gets
a real browser, the Ecto sandbox, and seed data from the test process.
PhxDemo.Case adds video recording, an overlay cursor, and a timeline
of what happened when. The steps in PhxDemo.Steps add pacing.
defmodule MyAppWeb.Demos.PostsDemo do
use PhxDemo.Case
use MyAppWeb, :verified_routes
demo "create a post", %{conn: conn} do
conn
|> scene("Creating a post")
|> visit(~p"/posts")
|> say("Here are the posts we already have.")
|> glide_click_link("New Post")
|> type_into({:label, "Title"}, "Hello world")
|> glide_click_button("Save Post")
|> assert_has("#flash-info", text: "Post created")
end
endEach demo writes into <output_dir>/<module>/<demo>/:
*.webm: the raw recording. Playwright finalizes it when the browser context closes, after the test's ownon_exitcallbacks.timeline.json: scenes, narration lines, and cursor targets with millisecond offsets from roughly the start of the recording.
Summary
Functions
Title card content for :intro / :outro settings, or nil.
Directory that demo output goes under.
Presentation settings for a demo, resolved at runtime. Priority: use PhxDemo.Case options, then config :phx_demo, <key>, then defaults.
Turns a module or demo name into a filesystem-safe slug.
Viewport and video size. Defaults to 1280x720.
Functions
Title card content for :intro / :outro settings, or nil.
iex> PhxDemo.card(:intro, true, "create a post", "my_app")
%{title: "Create a post", subtitle: "My App"}
iex> PhxDemo.card(:outro, true, "create a post", "my_app")
%{title: "My App", subtitle: "Thanks for watching"}
iex> PhxDemo.card(:intro, %{title: "Hi"}, "x", nil)
%{title: "Hi"}
iex> PhxDemo.card(:intro, false, "x", nil)
nil
Directory that demo output goes under.
Priority: :output_dir option, then config :phx_demo, :output_dir,
then tmp/demos.
Presentation settings for a demo, resolved at runtime. Priority: use PhxDemo.Case options, then config :phx_demo, <key>, then defaults.
:intro,:outro:true(generated title card),false, or%{title: ..., subtitle: ...}:captions::both(drawn into the video and a soft subtitle track),:burned,:soft, orfalse:zoom:true,false, or a max zoom factor (default 1.6):trim: cut dead time before the first step and after the last one:intro_ms,:outro_ms: card durations:music: path to a background track (any format ffmpeg reads, or a.mid/.midifile rendered with fluidsynth), a prompt to generate a track with ElevenLabs (%{elevenlabs: "calm lo-fi piano"}, seePhxDemo.MusicGen), orfalse(default). Plays whenever a path is set.:music_volume: music level in dB (default -18):soundfont:.sf2file used to render a MIDI:musictrack. Defaults to the one frommix phx_demo.soundfont.setup, then common system locations. SeePhxDemo.Midi.
Turns a module or demo name into a filesystem-safe slug.
iex> PhxDemo.slug("Create a Post!")
"create-a-post"
iex> PhxDemo.slug(MyAppWeb.Demos.PostsDemo)
"my_app_web-demos-posts_demo"
Viewport and video size. Defaults to 1280x720.