How to Automate Video Calls Testing (Step-by-Step)

How to Automate Video Calls Testing (Step-by-Step)

June 12, 2026 · 17 min read · How-To Guides

How to Automate Video Calls Testing (Step-by-Step)

Video conferencing has become a core feature in many products, and manual verification of every call flow is slow, error‑prone, and impossible to scale across browsers, devices, and network conditions. Automating video call testing gives you repeatable checks for connection setup, media exchange, UI correctness, and post‑call analytics while freeing QA to focus on exploratory work. This guide walks you through a complete, production‑ready approach: deciding when automation pays off, picking a framework, building stable locators, handling synchronization, managing test data, integrating with CI, reporting results, and using autonomous exploration to bootstrap the first tests without writing a single line of script. Each section includes concrete examples, code snippets, and tables you can copy into your own repository.

When Automation Pays Off for Video Calls

Cost of Manual Testing Versus Automated Runs

Manual testing of a video call typically requires a tester to launch two clients, sign in, negotiate a room, verify that local and remote video appear, check audio levels, hang up, and repeat for each browser/device combination. A single end‑to‑end scenario can take 5‑10 minutes of focused effort. If you need to run the same flow on Chrome, Firefox, Safari, and two mobile browsers across three network profiles (Wi‑Fi, 4G, throttled 3G), you already face 24 × 7‑10 min = 168‑280 minutes of tester time per release. Automating the same matrix reduces the execution time to a few minutes per agent because the script drives the browsers in parallel and eliminates repetitive UI navigation.

Sources of Flakiness Unique to Real‑Time Media

Video calls introduce non‑deterministic factors that rarely appear in traditional web UI tests: ICE connection state fluctuations, dynamic bitrate adaptation, temporary packet loss, and rendering delays caused by GPU scheduling. A test that merely asserts the presence of a tag can pass while the actual media stream is frozen. Recognizing these sources early helps you design waits and assertions that target media‑level state rather than superficial DOM changes.

Quick ROI Calculation

Estimate the hourly cost of a QA engineer (including overhead) at $50. If manual execution of your video call matrix consumes 4 hours per release, that is $200. An automated suite that runs in 15 minutes on a CI agent costs roughly $12.50 in compute (assuming a modest $50/hour VM rate). Even after adding script maintenance, the break‑even point is reached after fewer than three releases, making automation a clear win for any team that ships video functionality more than quarterly.

Choosing a Testing Framework for Real‑Time Media

WebDriver‑Based Options: Selenium, Appium, Playwright

Selenium/WebDriver remains the most portable choice for cross‑browser desktop testing. Appium extends Selenium to native mobile apps, allowing you to test Android/iOS video clients with the same language bindings. Playwright, while newer, offers built‑in support for multiple browser contexts, automatic waiting, and the ability to intercept WebSocket traffic—useful for signaling inspection. All three can drive the browser’s getUserMedia API and inspect and elements.

Specialized Media Testing Tools

Tools such as Janus‑Gateway test clients, mediasoup‑demo, and OpenVidu Call provide programmable endpoints that simulate participants and expose media statistics via REST or WebSocket. Pairing these with a UI driver lets you verify both the client UI and the server‑side media pipeline. Commercial offerings like TestRTC or Cypress‑real‑world‑app add pre‑built helpers for measuring MOS, jitter, and packet loss directly from the captured streams.

Selection Criteria

CriterionSelenium/WebDriverAppiumPlaywrightMedia‑Specific (Janus/OpenVidu)
Browser coverageChrome, Firefox, Safari, EdgeSame + mobile emulationChrome, Firefox, Safari, EdgeAny (via embedded player)
Mobile native supportNoYes (Android/iOS)Limited (via web view)No (requires wrapper)
Built‑in waitingBasic explicit waitsBasic explicit waitsAuto‑wait + network idleDepends on wrapper
ICE / signaling inspectionRequires custom codeSameCan intercept WebSocketsNative stats API
Setup complexityLowMedium (device farms)Low‑MediumMedium (media server)
LicenseApache 2.0Apache 2.0Apache 2.0Varied (MIT/AGPL)

If your primary goal is to validate UI interactions across desktop browsers, start with Playwright for its concise syntax and auto‑waiting. If you need to test native mobile apps or want a single language stack for web and mobile, choose Appium. When you also need to assert media quality metrics, layer a media‑gateway client on top of whichever UI driver you pick.

Setting Up the Test Environment

Mock Media Servers and SFUs

A reliable test suite needs a controllable media path. Instead of relying on a public SaaS (which introduces external variability), deploy a lightweight Selective Forwarding Unit (SFU) such as mediasoup, Janus Gateway, or OpenVidu behind Docker. These SFUs let you:

Example docker-compose.yml snippet for mediasoup:


version: "3.8"
services:
  mediasoup:
    image: mediasoup/media-soup-demo:latest
    ports:
      - "4443:4443"
      - "3000:3000"
    environment:
      - MEDIASOUP_LISTEN_IP=0.0.0.0
      - MEDIASOUP_MIN_PORT=40000
      - MEDIASOUP_MAX_PORT=40100
    command: ["node", "demo/server"]

Generating Fake Video and Audio Streams

Real cameras and microphones are unnecessary for automated tests. Use the browser’s getUserMedia with mediaSource set to a MediaStream generated from a canvas or an audio oscillator. The following helper creates a static color video track and a sine‑wave audio track:


function getFakeStream() {
  const canvas = document.createElement('canvas');
  canvas.width = 640;
  canvas.height = 480;
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'rgb(0,120,215)';
  ctx.fillRect(0,0,canvas.width,canvas.height);
  const videoTrack = canvas.captureStream(30).getVideoTracks()[0];

  const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  const oscillator = audioCtx.createOscillator();
  oscillator.type = 'sine';
  oscillator.frequency.value = 440; // A4
  const destination = audioCtx.createMediaStreamDestination();
  oscillator.connect(destination);
  oscillator.start();
  const audioTrack = destination.stream.getAudioTracks()[0];

  return new MediaStream([videoTrack, audioTrack]);
}

In your test, replace the real getUserMedia call with a stub that returns getFakeStream(). Playwright makes this easy via page.route:


await page.route('**/getUserMedia', route => {
  const fakeStream = page.evaluate(() => getFakeStream());
  route.fulfill({ body: JSON.stringify({ stream: fakeStream }) });
});

Containerizing Test Agents

Package your test runner (Node.js, Python, or Java) inside a Docker image that includes the browsers you need. Use the official playwright image for Chrome/Firefox or the selenium/standalone-chrome image for Selenium. Mount a shared volume for artifacts (video recordings, logs, traces). A minimal Dockerfile for a Playwright‑based suite looks like:


FROM mcr.microsoft.com/playwright:v1.42.0-focal
WORKDIR /tests
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "test"]

Push the image to your registry and let your CI pull it for each job. This guarantees identical browser versions and OS libraries across local and remote runs.

Designing Stable Locators for Video UI Elements

Avoiding Dynamic IDs and Class Names

Video call UIs often generate random IDs for elements like participant panels, mute buttons, or video containers. Relying on these leads to brittle tests. Instead, request that developers add stable data-testid attributes (or aria-labels) to every interactive component. Example HTML:


<div data-testid="local-video" class="video-container">
  <video autoplay muted playsinline></video>
</div>
<button data-testid="mute-button" aria-label="Toggle mute">
  <span>Mute</span>
</button>

Your locator then becomes a simple CSS selector: [data-testid="local-video"] video.

Using Accessibility Attributes as Fallbacks

If modifying the source is not possible, leverage ARIA labels, role attributes, or visible text that is unlikely to change. For instance, a “Leave call” button often has aria-label="Leave call" or contains the exact text “Leave call”. Use XPath with normalize-space() to trim whitespace:


//button[normalize-space()='Leave call']

Handling Canvas/WebGL Rendered Video

Some platforms render video into a element for effects or compositing. In such cases, the tag may be hidden (display:none) and the actual frames appear in the canvas. To verify that media is flowing, check that the canvas width and height are non‑zero and that its pixel data changes over time. A helper function in JavaScript:


async function canvasHasMovingContent(page, selector, samples=5, interval=100) {
  const ctx = await page.evaluateHandle(sel => {
    const c = document.querySelector(sel);
    return c.getContext('2d');
  }, selector);
  let prevData = null;
  for (let i = 0; i < samples; i++) {
    const data = await ctx.evaluateHandle(c => c.getImageData(0,0,c.width,c.height).data);
    const arr = await data.json();
    if (prevData && !arr.every((v,i)=>v===prevData[i])) return true;
    prevData = arr;
    await page.waitForTimeout(interval);
  }
  return false;
}

Call this after you expect remote video to start rendering; a true result indicates that the SFU is delivering frames.

Table: Locator Strategies and When to Use Them

StrategyProsConsTypical Use Case
data-testid attributeStable, explicit, immune to redesignRequires dev cooperationButtons, input fields, video containers
aria-label / roleWorks without markup changes, supports a11yMay be localized or changed for accessibilityIcons, menu items, toggle switches
Visible text (normalize-space)No attribute needed, easy to readFragile to copy changes, i18n issuesStatic labels, confirmation dialogs
Canvas pixel change detectionVerifies actual media renderingMore complex, slightly slowerWhen video is drawn to canvas/WebGL
CSS nth‑child or class patternsNo markup change neededHighly brittle to layout adjustmentsLast‑resort fallback only

Handling Waits, Synchronization, and Flaky Conditions

Explicit Waits for ICE Connection State

WebRTC exposes the RTCPeerConnection.iceConnectionState property, which progresses through new, checking, connected, completed, failed, disconnected, closed. A test should wait until the state reaches connected (or completed) before asserting media availability. In Playwright you can expose this via page.evaluate:


async function waitForIceConnected(page, timeout=15000) {
  await page.waitForFunction(() => {
    const pc = window.__lastPC__; // assume you store the peer connection globally
    return pc && pc.iceConnectionState === 'connected';
  }, { timeout });
}

You can store the peer connection when the app creates it (many frameworks expose it on window for debugging) or instrument the code with a wrapper that pushes the object to a known variable.

Detecting Remote Video Render

Even after ICE succeeds, the remote element may stay at zero width/height until the first frame arrives. A reliable wait checks for non‑zero video dimensions *and* a rising videoWidth property:


async function waitForRemoteVideo(page, selector, timeout=20000) {
  await page.waitForFunction(sel => {
    const v = document.querySelector(sel);
    return v && v.videoWidth > 0 && v.videoHeight > 0;
  }, { timeout }, selector);
}

If the app uses a canvas, replace the condition with the canvasHasMovingContent helper defined earlier.

Custom Expected Conditions for Media Metrics

Many SFUs publish stats via getStats(). You can poll for a minimum bitrate or frame rate to confirm that the stream is not stuck at a low fallback. Example in JavaScript:


async function waitForMinVideoBitrate(page, minKbps=100, timeout=30000) {
  await page.waitForFunction(async () => {
    const pc = window.__lastPC__;
    if (!pc) return false;
    const report = await pc.getStats();
    let bits = 0;
    report.forEach(value => {
      if (value.type === 'outbound-rtp' && value.mediaType === 'video') {
        bits += value.bytesSent * 8;
      }
    });
    // convert cumulative bytes to average kbps over the last second (simplistic)
    return bits / 1000 >= minKbps;
  }, { timeout });
}

Retry Patterns for Intermittent Failures

Network jitter can cause occasional ICE failures that recover on a retry. Wrap the core call flow in a retry loop with exponential backoff, but limit attempts to avoid masking real bugs:


async function withRetry(fn, retries=3, baseDelay=500) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === retries-1) throw err;
      await new Promise(r => setTimeout(r, baseDelay * 2**i));
    }
  }
}

Apply withRetry to the sequence that signs in, creates a room, and joins the call.

Data Setup, User Accounts, and Teardown Strategies

Provisioning Test Users via API

Most video platforms expose a REST endpoint to create temporary users or generate JWT tokens. Use a dedicated service account with rights to create many short‑lived accounts. Store credentials in a vault (e.g., AWS Secrets Manager, HashiCorp Vault) and fetch them at test start. Example in Python using requests:


def create_test_user():
    resp = requests.post(
        "https://api.example.com/v1/users",
        json={"displayName": f"tester-{uuid.uuid4()}"},
        headers={"Authorization": f"Bearer {SERVICE_TOKEN}"}
    )
    resp.raise_for_status()
    data = resp.json()
    return data["userId"], data["token"]

Managing Room Lifecycle

Create a unique room identifier for each test run (UUID or timestamp). After the test ends, issue a DELETE request to the room endpoint to free server resources. If the platform does not support deletion, rely on a short TTL (time‑to‑live) configured on the SFU side; ensure your test waits longer than the TTL before assuming the room is gone.

Cleaning Up Media Recordings

If your system automatically records calls, delete those artifacts after verification to avoid storage buildup. Most recording services provide a recordingId in the call‑completed webhook; call the delete API with that ID. In cases where recordings are stored in an S3 bucket, issue a DELETE Object request.

Teardown Hooks in Test Frameworks

Register afterEach or afterAll hooks to guarantee cleanup even when a test fails. In Playwright (Node.js):


afterAll(async () => {
  await api.deleteRoom(roomId);
  await api.deleteUser(userId);
});

In pytest with fixtures:


@pytest.fixture
def test_context():
    user_id, token = create_test_user()
    room_id = create_room(token)
    yield {"user_id": user_id, "token": token, "room_id": room_id}
    delete_room(room_id, token)
    delete_user(user_id, SERVICE_TOKEN)

Writing Maintainable Test Scripts

Page Object Model Adapted for Video Calls

Create a CallPage class that encapsulates all UI interactions and media checks. Keep locators private and expose high‑level methods like joinRoom(token), muteLocalAudio(), isRemoteVideoVisible(), getCallStats(). This isolates UI changes to a single file.


class CallPage {
  constructor(page) {
    this.page = page;
    this.localVideo = '[data-testid="local-video"] video';
    this.remoteVideo = '[data-testid="remote-video"] video';
    this.muteBtn = '[data-testid="mute-button"]';
    this.leaveBtn = '[data-testid="leave-button"]';
  }

  async joinRoom(token) {
    await this.page.goto(`https://app.example.com/room?token=${token}`);
    await this.page.waitForSelector(this.localVideo);
  }

  async muteLocalAudio() {
    await this.page.click(this.muteBtn);
  }

  async isRemoteVideoVisible(timeout=5000) {
    return await this.page.isVisible(this.remoteVideo, { timeout });
  }

  async getCallStats() {
    return await this.page.evaluate(() => {
      const pc = window.__lastPC__;
      return pc ? pc.getStats() : null;
    });
  }

  async leave() {
    await this.page.click(this.leaveBtn);
  }
}

Helper Library for Media Assertions

Extract repetitive checks into a utility module. Examples:

Parameterizing Scenarios with Data Files

Store variations (different codecs, network profiles, participant counts) in JSON or YAML files. Use a data‑driven test runner to iterate over each entry. In Jest:


test.each([
  { codec: 'VP8', loss: 0, latency: 0 },
  { codec: 'H264', loss: 0.02, latency: 80 },
  { codec: 'AV1', loss: 0.05, latency: 120 },
])('call succeeds with $codec under $loss loss and $latency ms latency', async ({codec, loss, latency}) => {
  await setupNetwork({ loss, latency });
  await callPage.joinRoom(token);
  await callPage.isRemoteVideoVisible();
  const stats = await callPage.getStats();
  expect(stats.codec).toBe(codec);
});

Keeping Tests Independent

Avoid sharing state between tests (e.g., a logged‑in session). Each test should sign in fresh, create its own room, and clean up afterward. This enables parallel execution without race conditions.

Integrating with CI/CD Pipelines

Choosing the CI Agent

Most CI systems (GitHub Actions, GitLab CI, Jenkins, Azure Pipelines) support Docker containers. Use the same image you built locally to guarantee parity. If you need GPU acceleration for hardware‑accelerated video decoding, select a runner with GPU access or use a cloud provider that offers it (e.g., Google Cloud’s n1-standard-4 with nvidia-tesla-t4).

Parallel Execution Strategies

Split your test matrix by browser × networkProfile × participantCount. Each combination becomes a separate job. In GitHub Actions:


strategy:
  matrix:
    browser: [chromium, firefox, webkit]
    profile: [wifi, lte3g, throttled]
    users: [2, 4, 8]

Collecting Artifacts

After each run, archive:

Upload these as workflow artifacts so developers can download and inspect them without rerunning the test.

Handling Flaky Test Reruns

Configure your CI to automatically retry a failed test up to two times before marking it a failure. Most CI platforms have a retry keyword. In Playwright’s test runner you can also set retries: 2 in playwright.config.ts. This reduces noise caused by transient network glitches while still surfacing genuine regressions.

Example GitHub Actions Workflow


name: Video Call CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/yourorg/video-call-tests:latest
      options: --gpus all   # if GPU needed
    strategy:
      matrix:
        browser: [chromium, firefox]
        profile: [wifi, lte3g]
    steps:
      - uses: actions/checkout@v3
      - name: Install deps
        run: npm ci
      - name: Run tests
        env:
          PLAYWRIGHT_BROWSERS_PATH: 0
        run: npx playwright test --project=${{ matrix.browser }} --retries=2
      - name: Upload artifacts
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: video-artifacts-${{ matrix.browser }}-${{ matrix.profile }}
          path: test-results/**/*

Reporting and Metrics for Video Call Tests

JUnit XML and TestRail Integration

Most test runners generate JUnit XML by default. Feed this into your test management tool (TestRail, Zephyr, Xray) to get a historic pass/fail trend per video call scenario. Add a custom property to each test case that records the average MOS (Mean Opinion Score) computed from captured audio.

Computing MOS from Captured Streams

If you record the remote audio track (using MediaRecorder or FFmpeg), you can run an objective quality estimator like PESQ or POLQA to approximate MOS. A simple FFmpeg command to extract audio and run PESQ via the libpesq filter:


ffmpeg -i input.webm -vn -ac 1 -ar 16000 audio.pcm
pesq +16000 audio.pcm reference.pcm +mos

Store the resulting MOS value as a test annotation.

Dashboard for Media KPIs

Create a Grafana dashboard that pulls from a time‑series database (Prometheus, InfluxDB) where each test run pushes metrics:

Panel examples: a heatmap of packet loss across network profiles, a line chart of MOS over time, and a bar chart of setup time per browser.

Linking Logs to Test Outcomes

When a test fails, automatically include a link to the corresponding SFU logs and the recorded media files in the test report. In your CI step, after a failure, run a script that uploads the logs to an artifact store and writes a markdown comment with URLs.


if [ $? -ne 0 ]; then
  curl -F "file=@sfu.log" https://artifactstore.example.com/upload
  echo "SFU log: https://artifactstore.example.com/upload/sfu.log" >> $GITHUB_STEP_SUMMARY
fi

Leveraging Autonomous Exploration to Bootstrap Video Calls Testing (SUSA)

How Autonomous Exploration Works

SUSA uploads your APK (for Android) or points at a web URL, then launches a fleet of virtual users with distinct personas—curious, impatient, novice, adversarial, elderly, accessibility‑focused, power user, etc. Each persona follows its own behavior model: a curious user taps every visible element, an impatient user skips tutorials, an accessibility user relies on screen‑reader cues, and an adversarial user attempts malformed inputs. While exploring, SUSA records every interaction, network request, and UI state change, building a graph of reachable screens and detecting dead ends, crashes, ANRs, and WCAG violations.

Generating Initial Video Call Scripts Without Manual Coding

When SUSA encounters a video call screen, it automatically:

  1. Detects the presence of a or element that receives media streams.
  2. Records the sequence of taps or clicks needed to join a room (e.g., “Enter meeting ID”, “Press Join”).
  3. Captures the signaling messages (WebSocket or HTTP POST) exchanged with the SFU.
  4. Emits a starter test script in the language of your choice (JavaScript/TypeScript for Playwright, Java for Appium, Python for Selenium) that reproduces the observed flow, inserts placeholder waits for media readiness, and adds basic assertions (local video present, remote video appears after ICE connected).

You can download this generated script from the SUSA dashboard, commit it to your repository, and immediately begin refining it—adding data‑driven parameters, custom media checks, and CI integration—without having to write the first line of UI navigation code from scratch.

Cross‑Session Learning Improves Stability Over Time

Each subsequent SUSA run remembers which locators proved stable (e.g., data-testid attributes that never changed) and which were volatile (auto‑generated IDs). It updates its internal selector heuristics, so the next generation of scripts is more resilient. Over several weeks, the proportion of flaky selectors drops dramatically, reducing the maintenance burden on your team.

When to Use SUSA Versus Hand‑Crafted Tests

Checklist for Reliable Video Call Automation

✅ ItemWhy It Matters
Use stable data-testid or ARIA labelsPrevents locator breakage on UI tweaks
Wait for ICE connected state before media checksGuarantees that the transport layer is ready
Verify non‑zero video dimensions or canvas pixel changeConfirms actual frame delivery, not just DOM presence
Capture and assert media stats (bitrate, fps, packet loss)Detects degradation that UI‑only checks would miss
Clean up rooms, users, and recordings after each testAvoids resource leaks and cross‑test contamination
Run tests in parallel across browsers & network profilesFinds environment‑specific issues early
Store logs, HAR files, and recordings as artifactsEnables post‑mortem debugging without rerunning
Configure automatic retries (max 2) for transient flukesReduces noise while preserving signal of real regressions
Monitor MOS or objective quality metrics per runConnects test outcome to perceived user experience
Review SUSA‑generated scripts monthly for selector driftKeeps auto‑generated baseline in sync with evolving UI

Closing Takeaways

Automating video call testing is no longer a luxury; it is a necessity for any product that relies on real‑time communication as a core feature. Begin by identifying the scenarios where manual effort outweighs the cost of script creation—typically regression suites, cross‑browser matrices, and performance‑sensitive flows. Choose a framework that gives you both UI control and access to WebRTC internals; Playwright offers a strong out‑of‑the‑box balance for web, while Appium extends the same skills to native mobile. Build your test environment around a controllable SFU and synthetic media streams so you can manipulate codecs, packet loss, and bandwidth without depending on flaky third‑party services.

Invest in stable locators (data-testid or ARIA) and explicit waits that target ICE connection state and actual media rendering. Encapsulate these actions in a Page Object Model and a small helper library for media assertions, then drive variations with data‑driven files. Integrate the suite into your CI pipeline using Dockerized agents, parallel matrices, and artifact collection to give developers immediate feedback when a call breaks. Enrich your JUnit reports with objective quality metrics such as MOS or bitrate, and visualize trends in Grafana to spot regressions before they reach users.

Finally, let autonomous exploration tools like SUSA bootstrap the first generation of tests. Their persona‑driven crawling produces realistic walks generate usable scripts, highlight volatile selectors, and evolve with each run, giving you a head start without writing UI navigation code from scratch. Treat those generated scripts as a living baseline: refine them, add your own assertions, and commit them to version control. With this combination of disciplined engineering practices and smart tooling, you will achieve fast, reliable, and repeatable verification of video calls—freeing your team to focus on the features that truly delight users.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free