Header Logo

How to Add End-to-End Tests for a Django WebRTC Voice Agent with Playwright

How to Add End-to-End Tests for a Django WebRTC Voice Agent with Playwright

Playwright E2E testing for Django WebRTC voice agents: mic permissions, session setup, connected state, and UI assertions.

Introduction


End-to-end testing a Django WebRTC voice agent is mostly about proving the full path works: browser microphone capture, signaling, session setup, agent audio playback, and the UI state that wraps it all. Unit tests can tell you the agent function returns the right message. They cannot tell you that the browser can actually join, negotiate media, and hear a response without hanging on permissions or ICE failure.


By the end of this post you should be able to build a Playwright test that:


  • loads your Django app in a real browser,

  • grants microphone permission in the test context,

  • starts a voice session,

  • waits for the agent to connect and produce audio/video state, and

  • asserts on the user-visible behavior instead of internal implementation details.


The example assumes a Django app that renders a page with a “start call” button and attaches to a WebRTC-backed voice agent. The same pattern applies whether your backend uses LiveKit directly, an API-managed session, or a wrapper service.


What makes WebRTC voice agents tricky to test


WebRTC tests fail for reasons that are easy to miss if you only run them locally:


  • Browser permissions: microphone access must be granted before getUserMedia() can succeed.

  • Network timing: signaling, ICE gathering, and connection establishment are asynchronous and nondeterministic.

  • Media is not a DOM concept: the call can be alive even if your app never updates the UI, and vice versa.

  • Backend coordination: Django may need to mint session tokens, create rooms, or hand out ephemeral credentials before the browser can connect.


The practical answer is not to mock all of that. Instead, keep the test close to production and make the assertions at the UI boundary: connected status, transcript rendering, audio activity indicators, avatar presence, and graceful disconnect/reconnect behavior.


Design the app so it is testable


Before writing the test, make the page expose a few stable selectors and status markers. Do not assert on implementation details like exact WebRTC events or transient spinner text. Use data attributes and coarse state labels.


A small example:


<button data-testid="start-call">Start call</button>
<div data-testid="transcript"><

<button data-testid="start-call">Start call</button>
<div data-testid="transcript"><

<button data-testid="start-call">Start call</button>
<div data-testid="transcript"><


In your front end, update call-status through clear states such as idle, connecting, connected, and ended. If you show a transcript, make sure it is deterministic enough for assertions. For example, you can render the first assistant response or a connection acknowledgment once the session is live.


On the Django side, separate session creation from page rendering. The page should request a session token or room join payload from the backend when the user clicks “Start call.” That keeps the initial page load simple and makes it easier to test the session bootstrap independently.


Use Playwright to control browser permissions and media


Playwright is well suited here because it can run a real browser and let you control permissions at the context level. For voice agents, you usually want to:


  1. launch Chromium with a predictable test origin,

  2. grant microphone permission,

  3. optionally route audio to a fake input device if your setup requires it,

  4. wait for your app to move into the connected state, and

  5. assert on visible evidence of a live session.


Here is a minimal pytest-style test:


import re
import re
import re


This is intentionally simple. The key is the wait strategy: do not sleep for an arbitrary number of seconds. Wait for a state change that your app controls. If your agent can take a while to warm up, make the test timeout explicit and generous enough to cover normal CI variance.


Make the backend session flow observable


For a Django app, the most common failure mode is that the page loads, but the backend never creates a valid session fast enough for the browser to join. Put the session bootstrap behind a dedicated endpoint and log the transition from request to room/session creation. Then the Playwright test can verify that the app shows a useful error if the endpoint fails.


Example shape of the flow:


POST /api/voice/session<br>-> UI switches to connected
POST /api/voice/session<br>-> UI switches to connected
POST /api/voice/session<br>-> UI switches to connected


If your app uses short-lived tokens, remember that flaky tests are often caused by token expiration, not WebRTC itself. Avoid reusing fixtures across too many tests, and generate fresh session credentials per test case. For CI, it is usually better to create a new session on each run than to cache anything involving auth or media state.


Also be explicit about failure handling. A good E2E test suite should cover at least one negative path: backend unavailable, session creation denied, or media permission blocked. Those tests are fast to write and save time later because they validate the UI you rely on when a real user cannot connect.


How to keep the test stable in CI


Voice-agent tests are inherently more brittle than regular browser tests, but a few constraints help a lot:


  • Use deterministic selectors: stick to data-testid attributes.

  • Wait on app state: assert on connected status, not on time elapsed.

  • Isolate the session: each test should create its own room or call session.

  • Keep the agent prompt predictable: a wildly variable first response makes transcript assertions noisy.

  • Run against a real browser: headless Chromium is usually enough, but do not over-mock WebRTC internals.


One practical trick is to expose a test-only mode in Django that shortens agent responses or uses a fixed opening phrase. That gives you a reliable assertion without changing production behavior. If your application needs to stream partial transcripts, assert on a stable prefix instead of a full sentence.


When the test fails, collect artifacts. Playwright traces, screenshots, and console logs matter more for these flows than for typical CRUD pages because the failure may be in signaling, media permissions, or a silent JS exception. Keep the debugging surface narrow: the test should tell you whether the page failed before join, during join, or after connection.


Where Protoface fits


If your Django app uses a realtime avatar layer on top of the voice agent, Protoface can be the piece that makes the end-to-end test more realistic without adding browser-side complexity. The useful part for this workflow is the LiveKit plugin: it drops a synchronized talking face into the agent, so your browser test can verify not just audio connectivity but also that the avatar video element actually becomes active.


For example, if your agent is built on LiveKit, you can wire in the plugin in your agent process and then assert in Playwright that the video surface is present and the call status is connected. The exact setup depends on your stack, but the pattern is straightforward: the browser joins as a user, the agent joins as a participant, and the avatar stream follows the agent’s speech in sync. If you need the integration details, the relevant quickstarts and docs are the place to look: documentation and the plugin repository at GitHub.


# illustrative only; exact config depends on your agent setup<p><
# illustrative only; exact config depends on your agent setup<p><
# illustrative only; exact config depends on your agent setup<p><


That makes the E2E test more meaningful: you are no longer just checking that a websocket connected. You are checking that a user can open the page, grant mic access, join a live session, and see a working audiovisual interaction.


A simple testing strategy that scales


In practice, I would split the coverage like this:


  • Backend tests: Django session creation, auth, and error handling.

  • Frontend unit tests: UI state transitions and selector coverage.

  • One or two Playwright E2E tests: happy path and one failure path.


That gives you confidence without turning CI into a media lab. The E2E tests should be slow enough to be valuable and fast enough that developers will still run them before merging.


Conclusion


The main idea is to test the real browser path, not a pile of mocked WebRTC internals. Grant microphone permission, create a fresh session, wait for a stable connected state, and assert on the UI evidence your users actually see. Keep the selectors deterministic, the session flow observable, and the agent response predictable enough for CI.


If you are adding a synced avatar layer to the agent, the LiveKit plugin path is a good fit for this kind of test because it gives you an additional, user-visible signal that the media pipeline is healthy. For setup details and exact API fields, start with the docs at docs.protoface.com and the relevant quickstarts linked from the project repositories.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.