Header Logo

Guide to Mocking WebRTC and WebSocket Connections in Vue 3 Avatar Tests

Guide to Mocking WebRTC and WebSocket Connections in Vue 3 Avatar Tests

Mock WebSocket and WebRTC in Vue 3 avatar tests with Vitest, jsdom, and event-driven fakes for deterministic UI state checks

Introduction


Testing realtime UI code is mostly about controlling the network boundary. With a Vue 3 avatar component, that boundary is usually a mix of WebSocket for signaling or event streams and WebRTC for media negotiation and playback. In production, those connections are noisy, timing-sensitive, and expensive to stand up in a test runner. In tests, you want the opposite: deterministic state changes, explicit message flow, and no dependency on cameras, microphones, STUN/TURN, or a live backend.


This guide shows how to mock both WebSocket and the WebRTC pieces a Vue 3 avatar component depends on. By the end, you should be able to unit test connection setup, reconnection behavior, message handling, and UI states like connecting, live, and error without leaving jsdom.


What actually needs to be mocked


For most avatar UIs, the browser-facing logic falls into three buckets:


  • Signaling transport: usually a WebSocket that carries session events, offers, answers, or control messages.

  • WebRTC primitives: RTCPeerConnection, MediaStream, and sometimes RTCSessionDescription / RTCIceCandidate.

  • Media elements: a <video> or <audio> element that receives the stream.


You do not need to fully emulate WebRTC internals for a component test. Usually you only need to assert that your component:


  1. opens the connection,

  2. reacts to incoming messages,

  3. creates or closes peer connections at the right time, and

  4. attaches a stream to the DOM when the session becomes active.


That means your mock should be event-driven, not feature-complete.


Mocking WebSocket in a predictable way


The simplest useful mock is a small class that records sent messages, exposes onopen/onmessage/onclose, and gives tests a way to trigger those events manually.


class MockWebSocket {

}
class MockWebSocket {

}
class MockWebSocket {

}


In your test setup, replace the global constructor:


beforeEach(() => {
});
beforeEach(() => {
});
beforeEach(() => {
});


This lets you write tests like: “when the component mounts, it opens a socket; when the server sends session.ready, the UI flips to connected; when the socket closes, the component shows an error.” The key is that the test owns the timeline.


Mocking WebRTC without pretending to be a browser


Testing code that uses RTCPeerConnection is where people usually over-mock. You do not want a fake browser media stack; you want just enough surface area for your component logic to run.


A practical mock usually includes:


  • createOffer / createAnswer returning resolved promises,

  • setLocalDescription / setRemoteDescription as no-ops that update internal state,

  • addTrack and addTransceiver as recorded calls,

  • event hooks for onicecandidate, onconnectionstatechange, and ontrack.


class MockRTCPeerConnection {

}
class MockRTCPeerConnection {

}
class MockRTCPeerConnection {

}


For stream attachment, mock MediaStream as a lightweight object and assert that your component assigns it to the video element:


class MockMediaStream {

}
class MockMediaStream {

}
class MockMediaStream {

}


In many Vue tests, that is enough. If your code depends on browser-specific behavior like ICE gathering or autoplay policies, keep those in an integration test layer and do not force unit tests to simulate them.


Testing a Vue 3 avatar component with Vitest and Vue Test Utils


A clean test usually verifies state transitions, not browser internals. Suppose your component does something like:


  • open a signaling socket on mount,

  • wait for a session.ready message,

  • create a peer connection,

  • send an SDP offer,

  • attach the incoming stream to a video element.


Your test can drive that flow explicitly:


import { mount } from "@vue/test-utils";

});
import { mount } from "@vue/test-utils";

});
import { mount } from "@vue/test-utils";

});


If your component stores the peer connection in a composable instead of on vm, adapt the assertion to the public DOM state. The test should verify observable behavior, not implementation details, unless the internal contract itself is part of the component’s API.


Gotchas that make these tests flaky


Mocking realtime code is easy to get almost right and still end up with brittle tests. The common failure modes are:


  • Implicit timers: if the component uses retries, debouncing, or async socket setup, drive them with fake timers and flush microtasks deliberately.

  • Shared global state: reset WebSocket, RTCPeerConnection, and any static instance arrays between tests.

  • Over-asserting implementation: checking exact SDP text or exact sequence of internal methods tends to break when you refactor without changing behavior.

  • Autoplay and media element quirks: jsdom will not behave like Chrome. Attach srcObject, but do not expect actual playback.


A good rule is to mock only the boundary you own. For example, if your component gets a session token from props and then hands off transport to a composable, test the composable separately from the visual wrapper.


Where Protoface fits in this workflow


If you are testing a Vue component that consumes a realtime avatar session from Protoface, you usually want the UI tests to stay fake while the backend contract stays real. That is a good fit for the REST API and the browser embed model: generate or manage the session server-side, then keep your frontend tests focused on signaling and rendering behavior.


For example, a backend test or local setup might create a session with the API, while the frontend test mocks the socket and peer connection:


curl -X POST "https://api.protoface.com/<session-endpoint>" \
-d '{"quality_tier":"standard","voice":"...","instructions":"..."}'
curl -X POST "https://api.protoface.com/<session-endpoint>" \
-d '{"quality_tier":"standard","voice":"...","instructions":"..."}'
curl -X POST "https://api.protoface.com/<session-endpoint>" \
-d '{"quality_tier":"standard","voice":"...","instructions":"..."}'


The exact fields and endpoints are in the docs, but the pattern is stable: keep credentials and session creation off the browser, and keep component tests deterministic. If you are integrating from Python, the SDK can do the same orchestration from your test harness or backend service, again without teaching the Vue test about the real network path. See the documentation at docs.protoface.com for the actual request and SDK shapes.


Practical test strategy


The best setup is usually layered:


  1. Unit tests mock WebSocket and RTCPeerConnection to verify UI behavior and state transitions.

  2. Contract tests hit your backend or a controlled test environment to confirm you create sessions correctly and handle the expected message schema.

  3. End-to-end tests cover one or two critical paths in a real browser, because autoplay, permissions, and network negotiation are browser behaviors, not jsdom behaviors.


That split keeps the fast tests fast and the expensive tests purposeful. It also makes failures easier to read: if the Vue unit test fails, it is your state machine; if the browser test fails, it is probably a transport or media issue.


Conclusion


For Vue 3 avatar components, the right way to mock realtime is to treat WebSocket and WebRTC as event sources, not as full protocol stacks. Build small fakes that let tests drive open, message, track, error, and close events; assert on visible UI state; and leave real media negotiation to a smaller number of browser-level tests.


If you are wiring this into a Protoface-backed avatar flow, keep session creation and auth on the server side, then test the frontend as a pure consumer of session events. The docs at docs.protoface.com are the right place for the exact API, SDK, and integration details.

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.