Reducing Flaky Tests for Realtime Voice and Video Avatars in Vue 3

Vue 3 testing strategies to reduce flaky realtime voice/video avatar tests with explicit state, mocks, and deterministic timing.
Introduction
Flaky tests in realtime voice/video systems usually come from the same place: you are asserting on asynchronous behavior that is driven by networks, media pipelines, and browser runtimes, but your test harness is pretending everything is synchronous and deterministic. In a Vue 3 app that embeds a talking avatar, that mismatch shows up fast: playback starts a little late, a WebRTC track arrives in a different order on CI, a component rerenders before the session state settles, or a browser permission prompt changes the timing enough to fail an otherwise “green” test.
This post is about making those tests boring. By the end, you should be able to separate pure Vue logic from media-driven side effects, write stable component and integration tests for avatar sessions, and know which parts of the stack deserve mocks versus real end-to-end coverage.
Start by testing the right layer
The biggest mistake is trying to validate the full realtime avatar experience in a single UI test. A talking face is a composition of several asynchronous systems: your Vue component, session orchestration, WebRTC signaling, media tracks, and the voice agent behind the avatar. If you test all of that in one place, every source of nondeterminism leaks into the assertion.
Instead, split your tests into three layers:
Pure component tests for state transitions, prop handling, and emitted events.
Integration tests for session lifecycle and DOM changes triggered by async callbacks.
End-to-end smoke tests for a small number of real sessions against a controlled environment.
In Vue 3, this usually means using your component test runner to mock the avatar/session client, while keeping a small set of higher-level tests that exercise the actual transport and media plumbing.
Model realtime state explicitly in Vue 3
Flaky tests often come from implicit state: a component assumes “connected” means both signaling and media are ready, but in practice those transitions arrive separately. Make the state machine explicit in your component. For example, track idle, connecting, ready, speaking, and error as distinct states, and only render UI based on those states rather than on ad hoc booleans.
A testable Vue composable might look like this:
This is easier to test than “the avatar face eventually appears” because the assertions are tied to deterministic state changes. Your tests can drive the client events directly, then verify the DOM in each state.
Two practical gotchas:
Do not derive readiness from a single event unless you know that event is the final handshake. In realtime systems, “connected” and “media flowing” are often different milestones.
Use a stable key for the avatar widget. If Vue recreates the media container on every prop change, you will introduce test noise and real playback glitches.
Mock the network boundary, not the behavior you care about
For component tests, the right seam is the avatar/session client, not the DOM. You want to fake session creation, track attachment, and speech events, while leaving the Vue component and its reactivity intact.
A lightweight mock can drive your component through the same states it would see in production:
In tests, avoid arbitrary sleeps like await new Promise(r => setTimeout(r, 500)). Use your test runner’s timer control or await a specific DOM condition. If you need to wait for Vue to settle, wait for the next render tick plus the exact mocked event you scheduled. That keeps the test anchored to a known transition rather than wall-clock time.
Also be careful with assertions on media elements. A video element may exist before the underlying track is attached, and the track may attach before frames are visible. Prefer asserting that the session is in the expected state and that the correct callbacks were invoked. Reserve pixel or frame assertions for a tiny number of visual regression tests.
Control timing deterministically
Realtime systems are naturally race-prone. In CI, that gets amplified by slower CPUs, headless browsers, and background load. Three techniques help a lot:
Use fake timers for pure JS scheduling when your component relies on debounce, reconnect backoff, or delayed UI transitions.
Synchronize on events, not delays for session setup. Wait for “connected,” “track added,” or “speech_end,” whichever reflects the actual contract you need.
Keep media-dependent tests small so they do not compete with each other for browser resources or network capacity.
A common anti-pattern is asserting on “avatar started speaking within 2 seconds.” That turns a functional requirement into a timing requirement, which is exactly what flakes under load. A better test is to stub the speech event and assert that the component transitions to the speaking UI immediately when the event arrives.
When you do need an actual browser media test, keep the environment as controlled as possible: fixed viewport, consistent audio device handling, and no parallel tests sharing the same browser context. For WebRTC-driven flows, serial execution is often worth the runtime cost.
What to verify in an avatar UI, and what not to
The point of a realtime avatar is not to verify codec internals in your app test suite. You should verify the contract your app depends on:
The session is created with the right instructions, voice, and user context.
The UI reflects connection state correctly.
Speech events cause visible state changes.
Disconnects and errors surface predictable recovery paths.
Do not try to validate lip sync frame-by-frame in a normal component test. That belongs in platform-level testing, not in your Vue suite. Likewise, do not depend on exact event ordering across different browsers unless that ordering is part of your own abstraction. WebRTC signaling, track negotiation, and autoplay policies vary enough that brittle ordering assumptions become a maintenance burden.
For cleanup, always make teardown explicit. Unmount the component, close the session client, stop any tracks, and clear timers. A lot of “random” flakes are actually leaked state from the previous test.
How Protoface fits without making tests worse
Where this gets practical is the session boundary. With Protoface, your Vue app does not need to know how the avatar is rendered or synced internally; it just needs to create or attach a session and react to events. That makes it a good candidate for the mock boundary described above.
If you are creating sessions programmatically, the REST API is straightforward to isolate in tests by stubbing the network call. For example, a session creation request can be represented as a small JSON payload, with the exact fields documented in the API docs:
In application code, you can keep the same boundary with the Python SDK or your frontend session wrapper and only test that your Vue layer responds correctly to the session lifecycle. The key is that your UI tests should not depend on real network latency unless the purpose of the test is explicitly to cover that path. If you need the exact request/response shape, use the docs at docs.protoface.com rather than guessing fields in your tests.
If you are using a LiveKit-based voice agent, the same principle applies: mock the plugin boundary and assert on the events your agent emits into the app, not on the mechanics of the media transport itself. The plugin should be treated as an integration dependency, not as something your Vue tests need to simulate in full.
One useful pattern: record real events, replay them in tests
For stubborn bugs, it helps to capture a real event sequence once and replay it deterministically in your test suite. For example, record the order in which your app saw connected, track_added, speech_start, and speech_end during a successful session. Then build a replay fixture that emits that exact sequence with fixed delays.
This gives you a realistic integration path without depending on live network behavior in every run. It is also a good way to reproduce race conditions after you fix them. If a bug only appears when speech starts before the video element is mounted, you can encode that sequence in a test and keep it from regressing.
Conclusion
Flaky avatar tests are usually a design problem, not a testing problem. Once you separate Vue state from realtime transport, use explicit session states, and mock the client boundary instead of the browser itself, the suite gets much more stable. Keep the truly realtime paths small and intentional, and let the rest of your tests run against deterministic events.
If you are building a voice or video avatar experience and want a cleaner integration boundary, the docs and quickstarts are the right place to start: docs.protoface.com. For the Vue side, the core principle stays the same no matter which avatar provider or media stack you use: test your state machine, not the network’s mood that day.
