ScreenshotNeo

BlogHow-to

How to Automate Video Testing With Selenium

Use Selenium WebDriver to test video playback, pauses, seeks, and failures with explicit waits and HTML media state. Includes runnable Python, cURL, and Node.js examples.

By the ScreenshotNeo team4 October 202610 min read

Selenium can automate video tests by driving a real browser through WebDriver and checking the page’s HTML media element. A reliable test waits for observable media state—such as playback starting and currentTime advancing—instead of assuming that page load or a fixed sleep proves the video works.

This guide uses Python for the complete Selenium examples. It covers readiness, playback, pause, seek, failure diagnostics, and running tests across browser environments. The examples expect a test page you control with a native <video> element.

1. Set up Selenium and a deterministic video fixture

Use a page and media file intended for testing. A public video service introduces variables such as network availability, consent prompts, changing markup, and third-party player behavior. A controlled fixture makes failures easier to reproduce.

Install Selenium in a virtual environment:

python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell
# .venv\Scripts\Activate.ps1
python -m pip install selenium

Recent Selenium documentation describes Selenium Manager as handling browser and driver management by default. You can still configure a specific browser or driver where your environment requires it. Consult the Selenium documentation for current binding and browser details.

Example fixture markup:

<video id="player" controls preload="metadata">
  <source src="/fixtures/sample.mp4" type="video/mp4">
  Your browser does not support HTML video.
</video>

Serve the fixture from a local test server or test application. Ensure the test video is long enough for the playback and seek checks below, and that the browser can decode its format.

2. Wait for media readiness, then test playback

The readyState property ranges from HAVE_NOTHING to HAVE_ENOUGH_DATA. For a smoke test, waiting for metadata and a usable duration is often enough to begin; it does not prove the entire video will play without buffering. The HTMLMediaElement readyState reference explains the values.

The test below waits for metadata, clicks the visible player control through Selenium, handles a rejected play() promise separately, waits for the playing event, and confirms time advances.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException

URL = "http://127.0.0.1:8000/video-fixture"

options = webdriver.ChromeOptions()
# For a headed run, omit this argument.
options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)

try:
    driver.get(URL)
    video = wait.until(lambda d: d.find_element(By.CSS_SELECTOR, "video#player"))

    wait.until(lambda d: d.execute_script(
        "const v = document.querySelector('video#player');"
        "return v && v.readyState >= HTMLMediaElement.HAVE_METADATA"
        " && Number.isFinite(v.duration) && v.duration > 0;"
    ))

    # Start through the visible native controls, as a user would.
    video.click()

    # A user gesture may permit playback where script-initiated autoplay is blocked.
    wait.until(lambda d: d.execute_script(
        "const v = document.querySelector('video#player');"
        "return v && !v.paused;"
    ))
    wait.until(lambda d: d.execute_script(
        "return document.querySelector('video#player').currentTime > 0;"
    ))

    state = driver.execute_script("""
        const v = document.querySelector('video#player');
        return {
          paused: v.paused,
          ended: v.ended,
          currentTime: v.currentTime,
          duration: v.duration,
          readyState: v.readyState,
          networkState: v.networkState,
          currentSrc: v.currentSrc,
          error: v.error ? { code: v.error.code, message: v.error.message } : null
        };
    """)
    assert state["paused"] is False, state
    assert state["currentTime"] > 0, state
    print(state)
finally:
    driver.quit()

For a custom player, locate and click its actual play button instead of clicking the media element. If the player is inside an iframe, switch into the relevant frame before locating its controls. Cross-origin embedded players may also have provider-specific APIs and restrictions; there is no single Selenium interaction that covers every provider.

Why not call play() as the main UI test?

HTMLMediaElement.play() returns a promise. It may resolve later or reject, including when browser autoplay policy blocks script-initiated playback or when media cannot be played. A UI test should exercise the user-facing control. If a test specifically targets programmatic playback, inspect the promise result and assert the intended behavior rather than assuming success. See the play() reference.

result = driver.execute_async_script("""
  const done = arguments[arguments.length - 1];
  const v = document.querySelector('video#player');
  v.play().then(() => done({ok: true}))
    .catch(error => done({ok: false, name: error.name, message: error.message}));
""")
assert result["ok"], result

3. Test pause, seek, and end behavior

Keep each assertion tied to the behavior under test. Media events such as playing, pause, seeking, seeked, waiting, stalled, ended, and error distinguish transitions that a generic “page loaded” assertion misses. The HTMLMediaElement reference lists the media properties and events.

Pause assertion

video.click()  # Native controls toggle to pause when currently playing.
wait.until(lambda d: d.execute_script(
    "return document.querySelector('video#player').paused === true;"
))

With a custom control, click its pause button and then check paused. Avoid relying on button text alone: a control can look correct while the media state is unchanged.

Seek assertion

target_seconds = 5

driver.execute_script("""
  const v = document.querySelector('video#player');
  v.currentTime = arguments[0];
""", target_seconds)

wait.until(lambda d: d.execute_script(
    "const v = document.querySelector('video#player');"
    "return !v.seeking && Math.abs(v.currentTime - arguments[0]) < 0.75;",
    target_seconds
))

For a UI seek test, interact with the player’s timeline instead, then check that seeking completes and that the final position is near the target. Browsers can seek to a nearby decodable frame, so allow a small tolerance. Guard against target times outside the media duration and account for live streams, whose seekable range may move and whose duration may be infinite.

End assertion

For a short fixture, wait for the ended property or event. Do not infer completion from a fixed sleep. Keep the fixture short so the test remains practical, or seek close to the end and verify the player’s completion behavior separately.

4. Add explicit event waits when transitions matter

Polling state is convenient for smoke checks. When the event itself is part of the contract, register a listener before triggering the action so the test cannot miss a fast event.

driver.execute_script("""
  const v = document.querySelector('video#player');
  window.__videoEvent = null;
  v.addEventListener('playing', () => { window.__videoEvent = 'playing'; }, {once: true});
""")
video.click()
wait.until(lambda d: d.execute_script("return window.__videoEvent === 'playing';"))

Do not use time.sleep() as the normal synchronization mechanism. A fixed delay is either unnecessarily long on a fast run or too short on a slow one. Selenium’s wait strategies cover explicit waits for asynchronous conditions.

5. Diagnose failures with browser and media state

On failure, record enough context to distinguish a locator problem, a media load problem, a playback-policy rejection, and a buffering or network problem. Capture the browser and driver versions alongside media state.

diagnostics = driver.execute_script("""
  const v = document.querySelector('video#player');
  if (!v) return {videoFound: false};
  return {
    videoFound: true,
    currentSrc: v.currentSrc,
    currentTime: v.currentTime,
    duration: v.duration,
    paused: v.paused,
    ended: v.ended,
    readyState: v.readyState,
    networkState: v.networkState,
    error: v.error ? {code: v.error.code, message: v.error.message} : null,
    buffered: Array.from({length: v.buffered.length}, (_, i) => [
      v.buffered.start(i), v.buffered.end(i)
    ])
  };
""")
print(diagnostics)
print("browser capabilities:", driver.capabilities)

Also collect a screenshot and browser console or runtime errors where your test setup supports it. Selenium WebDriver BiDi can stream browser events such as network requests, console messages, and JavaScript errors. Selenium describes BiDi as evolving, so check current support in the chosen browser and language binding before making it a test requirement: Selenium BiDi documentation.

6. Choose local WebDriver or Selenium Grid

Approach Useful when Trade-off
Local WebDriver Developing a focused test or reproducing a failure in one browser. Coverage is limited to the machine’s installed browser environment.
Selenium Grid Running tests across browser and operating system combinations, distributing execution, or using remote machines. Requires Grid setup or access to a Grid environment and attention to remote session capabilities.

Grid’s purpose includes distributing tests across several machines and environments. See the Selenium Grid documentation. Keep a small deterministic video smoke test in the fast local path, then expand the browser matrix for compatibility coverage.

Remote WebDriver example

For a Grid endpoint, replace the local driver construction with a remote session. The browser option must match a browser slot configured on the Grid.

from selenium import webdriver

options = webdriver.ChromeOptions()
driver = webdriver.Remote(
    command_executor="http://localhost:4444",
    options=options
)
try:
    driver.get("http://host-accessible-to-grid:8000/video-fixture")
    # Run the same explicit-wait assertions here.
finally:
    driver.quit()

The fixture URL must be reachable from the browser node, not merely from the test runner. Use the Grid’s configured endpoint and capability conventions for your deployment.

7. cURL and Node.js alternatives for screenshot evidence

cURL does not drive Selenium or assert video behavior. It can request an HTTP resource such as a fixture page or media file, which is useful for checking reachability but cannot prove the browser played the video. Node.js can drive Selenium through its language binding; the example below starts a browser and waits on HTML media state.

cURL: check that a fixture responds

curl -I http://127.0.0.1:8000/fixtures/sample.mp4

A successful HTTP response confirms only that an endpoint responded. It does not validate browser decoding, playback policy, controls, or seek behavior.

Node.js Selenium smoke test

Install the JavaScript binding with npm install selenium-webdriver and configure the browser and driver in the environment as required by your Selenium version.

const { Builder, By, until } = require('selenium-webdriver');

(async () => {
  const driver = await new Builder().forBrowser('chrome').build();
  try {
    await driver.get('http://127.0.0.1:8000/video-fixture');
    const video = await driver.findElement(By.css('video#player'));
    await driver.wait(async () => driver.executeScript(
      "const v=document.querySelector('video#player');"
      + "return v && v.readyState >= 1 && v.duration > 0;"
    ), 15000);
    await video.click();
    await driver.wait(async () => driver.executeScript(
      "const v=document.querySelector('video#player');"
      + "return v && !v.paused && v.currentTime > 0;"
    ), 15000);
  } finally {
    await driver.quit();
  }
})().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

8. Common errors and fixes

Symptom Likely cause Fix
Video element is not found The page is still rendering, the selector is wrong, or the player is inside an iframe. Wait for the element, verify the selector, and switch into the correct frame before searching.
readyState never reaches metadata The source URL failed, the server is unreachable, or the fixture markup does not point to the expected media. Inspect currentSrc, network behavior, networkState, and error; verify the URL from the browser node.
play() rejects Autoplay policy blocked script playback, or the media is unsupported or failed. For UI coverage, click the visible play control. For programmatic playback tests, report the rejection name and message and verify the source separately.
Playback starts but time does not advance The media may be paused, waiting for data, stalled, or at its end. Check paused, ended, readyState, buffered ranges, and waiting/stalled/error events.
Seek assertion times out The target is outside the seekable range, the fixture is too short, or seeking has not completed. Check duration and seekable ranges, choose a valid target, and wait for seeked or seeking === false with a tolerance.
Works locally, fails on Grid The remote browser node cannot access the fixture host, or it differs in browser, codec, or environment. Use a host reachable by the node, log remote capabilities, and test the intended browser environment explicitly.
Headless playback differs from headed playback Browser configuration or environment affects rendering or media support. Reproduce in the target browser mode and environment; do not treat headless success as proof for every user setup.

9. Reliability, performance, and test cost

  • Control the media input. Use a stable fixture hosted with the test application when possible. A third-party source adds network and service variability.
  • Keep waits bounded. Set timeouts that reflect expected loading in the test environment, and include diagnostics on timeout instead of increasing delays blindly.
  • Test distinct behaviors separately. A short smoke test, a seek test, and an error-path test fail for clearer reasons than one long end-to-end scenario.
  • Be careful with parallelism. Browser sessions consume machine resources and media requests add network load. Increase parallel runs only while the environment remains stable.
  • Match claims to coverage. A browser smoke test establishes behavior for that browser, fixture, and run. It does not establish perceived audio/video quality, codec support on every hardware setup, or sustained live-stream performance. Add dedicated media, visual, or network testing for those goals.
  • Use Grid when breadth matters. Distributed environments broaden browser and OS coverage, with additional setup and execution overhead.

10. Or skip the browser setup

If you need a screenshot of the page or player state for a report, [ScreenshotNeo](https://screenshotneo.com) provides a website screenshot API and MCP server. Its API can capture a page in one GET request. A screenshot is useful as visual evidence, but it does not replace Selenium assertions about playback, seeking, or media state.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before capture; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can Selenium test an embedded YouTube or Vimeo player?

It can automate browser interactions, but embedded players have provider-specific controls, APIs, and cross-origin behavior. Check the provider’s current integration documentation and test the behavior that your application owns.

Does readyState 4 guarantee uninterrupted playback?

No. HAVE_ENOUGH_DATA is the browser’s estimate that enough data is available to play through without interruption. It is not a guarantee for a long video or live stream.

Should video tests run in every browser on every commit?

Choose the matrix based on your supported environments and execution budget. A quick deterministic smoke test can run frequently, with broader Grid coverage on a schedule or in the release path.

Can a screenshot prove the video works?

No. A screenshot can show the page’s visual state, but Selenium must inspect media state and transitions to verify playback behavior.