# `PhxDemo.Steps`
[🔗](https://github.com/thenatespack/phoenix_demo/blob/main/lib/phx_demo/steps.ex#L1)

Pipeable demo steps. Each takes and returns the PhoenixTest session (`conn`),
so they mix freely with `visit/2`, `click_link/2`, `assert_has/3`, etc.

`PhxDemo.Case` imports them. Outside a demo (in a plain `test`) they
still work but don't record to a timeline.

Targets are anything `PhxDemo.Target` accepts: a CSS selector string, or
`{:link, text}`, `{:button, text}`, `{:label, text}`, `{:text, text}`, `{:role, role, name}`.

# `await_liveview`

Waits until LiveView is connected and no `phx-*-loading` classes remain.
`glide/3` and the `glide_click_*` steps already call this. Use it after
plain PhoenixTest actions such as `visit/2` if you need a settled page.
See `PhxDemo.LiveView`.

# `await_uploads`

Waits for in-flight LiveView uploads to finish. `upload/4`, `drop_files/4`,
`glide/3` and the `glide_click_*` steps already do; use it after something
that starts an upload another way.

# `clear_spotlight`

Removes the spotlight.

# `drop_files`

Drags files onto a drop zone (an element with LiveView's `phx-drop-target`).

    conn |> drop_files("#drop-zone", "avatar.png")

File icons labeled with the file names fly in from off-screen to the zone
while the cursor glides there, then `dragenter`, `dragover` and `drop` are
dispatched with a `DataTransfer` holding the files' real bytes, built in the
page. It then waits for LiveView to finish uploading.

Bytes are sent to the page base64-encoded, so keep demo fixtures small.
Paths resolve as in `upload/4`. Records an `:upload` event with
`mode: "drop"`.

Options:
  * `:flight_ms`: how long the icons travel (default 900)
  * `:steps`: as in `glide/3`
  * `:settle`: milliseconds to hold after the upload finishes (default 300)

# `glide`

Glides the cursor to the center of `target`, scrolling it into view first.
Waits for LiveView to settle before measuring.

Options:
  * `:steps`: intermediate mouse moves (default 25; more is smoother and slower)

# `glide_click_button`

Glides to a button, then clicks it with `PhoenixTest.click_button/2`.

# `glide_click_link`

Glides to a link, then clicks it with `PhoenixTest.click_link/2`.

# `hide_cursor`

Hides the overlay cursor, e.g. for a scene that's only a static page.

# `mark`

A named marker in the timeline.

# `pause`

Waits so the viewer can take in what's on screen.

# `say`

A narration line. It becomes a caption, and, when a TTS adapter is
configured (see `PhxDemo.TTS`), spoken audio in the rendered MP4.

The demo pauses so the line can finish: for the real clip length plus
`:gap` with TTS, or for an estimated speaking time without.

Options:
  * `:wait`: milliseconds to pause instead, or `false` to not pause (the
    next steps then happen while the line is spoken)
  * `:gap`: extra pause after a TTS clip (default 250)

# `scene`

Starts a new scene: a chapter in the MP4. The previous scene's final state
is screenshotted for drift checks (`PhxDemo.Drift`).

# `show_cursor`

Shows the overlay cursor again.

# `speaking_ms`

Estimated time to speak `text` at about 150 words per minute, with a 900ms minimum.

    iex> PhxDemo.Steps.speaking_ms("one two three")
    1154

# `spotlight`

Dims the page around `target` and outlines it.

# `stop_camera`

Ends the fake camera: the tracks the app holds are stopped and fire `ended`,
and new `getUserMedia` video requests fail with `NotFoundError`.

# `stop_microphone`

Ends the fake microphone: the tracks the app holds are stopped and fire
`ended`, and new `getUserMedia` audio requests fail with `NotFoundError`.

# `type_into`

Glides to a form control and types into it one key at a time.

Options:
  * `:speed`: `:fast`, `:human` (default) or `:slow`, or milliseconds per key
  * `:steps`: as in `glide/3`

# `upload`

Uploads files through a file input, the way a person would: the cursor
glides to the input (or its label), a click shows, then the files are
attached and the step waits for LiveView to finish uploading them.

    conn
    |> upload("#doc-upload", "report.pdf")
    |> upload({:label, "Photos"}, ["a.png", "b.png"], picker: true)

`target` is the file input or its label (any `PhxDemo.Target`). A hidden
input is fine: the cursor goes to its label. Relative paths resolve from
`test/demos/fixtures/` (see `PhxDemo.Fixtures`); a missing file raises
`PhxDemo.Env.Error`, and literal paths are checked before the browser starts.

Playwright attaches files without a native dialog. LiveView's upload
progress is awaited with no sleeps (`PhxDemo.LiveView`), so the next step
sees the finished entries. In a demo this step shadows
`PhoenixTest.upload/3,4,5`; call that one qualified if you need it.

Records an `:upload` timeline event with `files` (`name`, `size`),
`mode: "input"`, the target and its `box`, which auto-zoom uses.

Options:
  * `:picker`: show a fake file-picker sheet with the files highlighted for
    about a second before attaching (default `false`)
  * `:picker_ms`: how long the sheet stays (default 1100)
  * `:steps`: as in `glide/3`
  * `:settle`: milliseconds to hold after the upload finishes (default 300)

# `upload_audio`

Uploads an audio file (voice note, podcast, recording) through a file input.
A thin wrapper over `upload/4` that also records an `:audio_input` timeline
event (`mode: "upload"`, `file`, `duration_ms` from ffprobe; its `at_ms` is
when the file was attached).

    conn |> upload_audio("#voice-note", "hello.wav")
    conn |> upload_audio("#drop-zone", "hello.wav", drop: true, audible: true)

`path` resolves as in `upload/4`. Options are those of `upload/4` (or
`drop_files/4` with `drop: true`) plus:

  * `:drop`: attach by dropping onto the target instead of the file input
    (default `false`)
  * `:audible`: mix the audio into the MP4 at that moment (default `false`).
    Music ducks under it as under `say`, and a warning is logged if it overlaps
    a spoken line. See `PhxDemo.Audio`.

# `use_camera`

Makes the page's camera requests (`getUserMedia({video: ...})`) return
frames from a video fixture. See `PhxDemo.Camera` for how it works and which
formats are supported (non-WebM videos are converted with ffmpeg and cached).

Calling it again switches the fixture on the live stream, so an app that is
already showing the camera sees the new video.

Options:
  * `:loop`: restart the video when it ends (default `true`)
  * `:audio`: use the video's soundtrack as the microphone (default `false`),
    played when the app requests audio. Also takes `:audible`, as in
    `use_microphone/3`. `stop_camera/1` stops it too.

# `use_camera_image`

Like `use_camera/3`, with a still image as the camera feed (QR or document scanning).

# `use_microphone`

Makes the page's microphone requests (`getUserMedia({audio: ...})`) return
audio from a fixture file. Playback starts when the app requests the
microphone, and the `:audio_input` timeline event (`mode: "microphone"`,
`action: "play"`) records that moment. See `PhxDemo.Audio` for the details and
formats (anything ffmpeg decodes; formats the browser can't decode are
converted to WAV and cached).

    conn
    |> use_microphone("question.wav")
    |> visit(~p"/record")
    |> glide_click_button("Record")

Calling it again switches the fixture; an app that is already listening hears
the new one. Relative paths resolve as in `upload/4`.

Options:
  * `:loop`: repeat the audio while the app listens (default `false`: it
    plays once per request)
  * `:audible`: mix the audio into the MP4 (default `false`). Music ducks
    under it as under `say`; a warning is logged if it overlaps a spoken line.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
