Platform

Solutions

Resources

Platform

Solutions

Resources

Platform

Solutions

Resources

Blog

Headless Browser Testing with Selenium: Complete Guide

Rishabh Kumar

Published on

eBook CTA Background
RECOMMENDED RESOURCES

The Ultimate Enterprise Guide To Agentic Testing

AI agents can write, fix and triage your tests at scale. See how to use them in QA while keeping people in control of everything that ships.

Get the eBook →

Enterprise guide to agentic testing
eBook CTA Background
RECOMMENDED RESOURCES

The Ultimate Enterprise Guide To Agentic Testing

AI agents can write, fix and triage your tests at scale. See how to use them in QA while keeping people in control of everything that ships.

Get the eBook →

Enterprise guide to agentic testing
eBook CTA Background
RECOMMENDED RESOURCES
Enterprise guide to agentic testing

The Ultimate Enterprise Guide To Agentic Testing

AI agents can write, fix and triage your tests at scale. See how to use them in QA while keeping people in control of everything that ships.

Get the eBook →

eBook CTA Background
RECOMMENDED RESOURCES

The Ultimate Enterprise Guide To Agentic Testing

AI agents can write, fix and triage your tests at scale. See how to use them in QA while keeping people in control of everything that ships.

Get the eBook →

Enterprise guide to agentic testing

Table of contents

Lorem ipsum

Lorem ipsum

Lorem ipsum

Lorem ipsum

Lorem ipsum

Headless browser testing runs automated tests without drawing the browser on screen. It's quicker to run, lighter on resources, and fits naturally into CI/CD pipelines, where servers usually have no display at all. Selenium supports headless modes for Chrome, Firefox and Edge, and since Selenium Manager arrived, getting started is much simpler than it used to be.

This guide covers how headless testing works, how to configure it in Selenium for each major browser, how to run it in Jenkins, GitHub Actions and Azure DevOps, the problems teams run into, and where local headless infrastructure starts to hold you back.

What is Headless Browser Testing

Headless browser testing runs automated tests in a browser that has no visible window. The browser still does everything it normally would: it loads pages, runs JavaScript, fills in forms, clicks buttons and navigates between pages. It just never draws any of it on screen.

Technical Definition

A headless browser is a full browser engine (Chromium, Gecko or WebKit) that executes JavaScript, processes CSS, builds the DOM and handles network requests, all without opening a window. You control it through code rather than a user interface.

Traditional Browser Execution:

Test script → WebDriver → Browser window opens
→ Page loads and renders on screen
→ Actions performed visibly → Results returned
Test script → WebDriver → Browser window opens
→ Page loads and renders on screen
→ Actions performed visibly → Results returned
Test script → WebDriver → Browser window opens
→ Page loads and renders on screen
→ Actions performed visibly → Results returned
Test script → WebDriver → Browser window opens
→ Page loads and renders on screen
→ Actions performed visibly → Results returned

Headless Browser Execution:

Test script → WebDriver → Browser starts with no window
→ Page loads and runs in memory
→ Actions performed in the background → Results returned
Test script → WebDriver → Browser starts with no window
→ Page loads and runs in memory
→ Actions performed in the background → Results returned
Test script → WebDriver → Browser starts with no window
→ Page loads and runs in memory
→ Actions performed in the background → Results returned
Test script → WebDriver → Browser starts with no window
→ Page loads and runs in memory
→ Actions performed in the background → Results returned

Historical Context

Early headless testing relied on tools like PhantomJS or HtmlUnitDriver, which imitated a browser without using a real browser engine. That meant tests could pass headlessly and still fail for real users. Modern headless testing uses the actual browsers (Chrome, Firefox and Edge) running in headless mode, so what you test behaves like what your users get.

Why Headless Browser Testing Matters

  1. Performance Optimization

Faster Execution Speed‍

Headless browsers skip the work of painting pixels to a screen, so tests usually finish faster than in a visible browser. The gain varies with the application, but on large regression suites with hundreds of tests, the time saved adds up across every run.

The browser still loads CSS, JavaScript and images and builds the page in full. It simply doesn't draw the result, which keeps behaviour realistic while cutting overhead.

  1. Resource Efficiency

Lower memory use

Without the display layer, GPU work and visual buffers, a headless browser typically uses noticeably less memory than a visible one.

Lower CPU use

With no screen painting, animations or visual effects to process, more CPU is left for running the tests themselves.

More parallel capacity

Because each instance is lighter, the same machine can run more headless browsers side by side than visible ones.

  1. CI/CD Pipeline Integration

Works on servers without displays

CI/CD servers in Jenkins, GitHub Actions and Azure DevOps usually run without a screen. Headless browsers run there naturally, with no need to configure a virtual display.

Enables continuous testing

Tests triggered on every commit, pull request or deployment run efficiently in headless mode, giving developers fast feedback.

Shorter build times

Faster test runs keep pipelines responsive, so pull requests get results in minutes rather than sitting in a queue.

  1. Cost and Scalability Benefits

Lower CI costs

Cloud CI runners charge by the minute. Faster, lighter tests mean fewer billable minutes across thousands of pipeline runs.

Easier scaling

Lightweight headless instances spread across several machines more easily, which makes distributed test runs more affordable.

Headless Browser Options for Selenium

  1. Headless Chrome (Chromium-Based)

Market Position‍

Chrome has the largest share of the desktop browser market, which makes headless Chrome the most common choice for automated testing.

Technical Capabilities:

  • The full Chromium engine, with complete JavaScript support

  • DevTools Protocol access for advanced debugging

  • Built-in PDF generation and screenshot capture

  • Strong performance and stability

  • Support for modern CSS and web standards

Selenium Configuration:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()

Note: Chrome introduced a new headless mode in version 109, and from Chrome 132 the old mode was removed entirely. Both --headless and --headless=new now run the new mode, which behaves almost exactly like visible Chrome. If you specifically need the old, lighter headless engine, Google now ships it separately as chrome-headless-shell.

You don't need to download ChromeDriver yourself. From Selenium 4.6 onwards, Selenium Manager finds or downloads the right driver automatically.

  1. Headless Firefox

Market Position‍

Firefox has a smaller share of users than Chrome, but it's still important for cross-browser testing because it uses a completely different engine. Bugs that never appear in Chromium can show up in Firefox.

Technical Capabilities:

  • The Gecko engine, with strong standards compliance

  • Robust privacy and security features

  • Good performance on modern web applications

  • Reliable handling of complex DOM structures

Selenium Configuration:

from selenium import webdriver

options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.add_argument("--width=1920")
options.add_argument("--height=1080")

driver = webdriver.Firefox(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.add_argument("--width=1920")
options.add_argument("--height=1080")

driver = webdriver.Firefox(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.add_argument("--width=1920")
options.add_argument("--height=1080")

driver = webdriver.Firefox(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.add_argument("--width=1920")
options.add_argument("--height=1080")

driver = webdriver.Firefox(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()

Alternative Syntax:

You can also switch Firefox into headless mode with an environment variable, which is handy in CI where you don't want to change the code:

export MOZ_HEADLESS=1
pytest tests

export MOZ_HEADLESS=1
pytest tests

export MOZ_HEADLESS=1
pytest tests

export MOZ_HEADLESS=1
pytest tests

The older options.headless = True property was removed in Selenium 4.10, so any code using it needs updating to one of the approaches above.

  1. Headless Edge

Market Position‍

Microsoft Edge has been built on Chromium since 2020. It matters most for organisations standardised on Microsoft tools, where it's often the default browser.

Technical Capabilities:

  • The Chromium engine, with performance similar to Chrome

  • Integration with Windows enterprise features

  • A good fit for testing Microsoft 365 and Azure-based applications

  • The same headless capabilities as Chrome

Selenium Configuration:

from selenium import webdriver

options = webdriver.EdgeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Edge(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.EdgeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Edge(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.EdgeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Edge(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
from selenium import webdriver

options = webdriver.EdgeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

driver = webdriver.Edge(options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
  1. What About Safari?

Safari doesn't support headless mode. SafariDriver only runs a visible browser, and only on macOS. If Safari coverage matters to your users, you'll need macOS machines running visible Safari, or a cloud platform that provides real Safari instances.

  1. HtmlUnitDriver (Legacy)

Historical Context

HtmlUnitDriver was Selenium's original headless option. It simulates a browser in Java rather than using a real browser engine.

Current Status

It's now maintained as a separate project and is rarely used for modern testing. Its JavaScript support and rendering accuracy lag well behind real browsers, so tests can pass in HtmlUnit and still fail for users.

// Requires the separate htmlunit-driver dependency
WebDriver driver = new HtmlUnitDriver(true); // true enables JavaScript
driver.get("https://example.com");
// Requires the separate htmlunit-driver dependency
WebDriver driver = new HtmlUnitDriver(true); // true enables JavaScript
driver.get("https://example.com");
// Requires the separate htmlunit-driver dependency
WebDriver driver = new HtmlUnitDriver(true); // true enables JavaScript
driver.get("https://example.com");
// Requires the separate htmlunit-driver dependency
WebDriver driver = new HtmlUnitDriver(true); // true enables JavaScript
driver.get("https://example.com");

When to Consider it

Only for very lightweight checks on simple pages where JavaScript barely matters and exact browser behaviour isn't important.

Complete Headless Testing Implementation

1. Python Implementation

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1920,1080")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


def test_user_can_sign_in(driver):
    driver.get("https://example.com/login")
    wait = WebDriverWait(driver, 10)

    wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("jane@example.com")
    driver.find_element(By.ID, "password").send_keys("S3cure!pass")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    heading = wait.until(EC.visibility_of_element_located((By.TAG_NAME, "h1")))
    assert "Welcome" in heading.text
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1920,1080")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


def test_user_can_sign_in(driver):
    driver.get("https://example.com/login")
    wait = WebDriverWait(driver, 10)

    wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("jane@example.com")
    driver.find_element(By.ID, "password").send_keys("S3cure!pass")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    heading = wait.until(EC.visibility_of_element_located((By.TAG_NAME, "h1")))
    assert "Welcome" in heading.text
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1920,1080")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


def test_user_can_sign_in(driver):
    driver.get("https://example.com/login")
    wait = WebDriverWait(driver, 10)

    wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("jane@example.com")
    driver.find_element(By.ID, "password").send_keys("S3cure!pass")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    heading = wait.until(EC.visibility_of_element_located((By.TAG_NAME, "h1")))
    assert "Welcome" in heading.text
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1920,1080")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


def test_user_can_sign_in(driver):
    driver.get("https://example.com/login")
    wait = WebDriverWait(driver, 10)

    wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("jane@example.com")
    driver.find_element(By.ID, "password").send_keys("S3cure!pass")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    heading = wait.until(EC.visibility_of_element_located((By.TAG_NAME, "h1")))
    assert "Welcome" in heading.text

2. JavaScript (Node.js) Implementation

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

(async function userCanSignIn() {
  const options = new chrome.Options()
    .addArguments('--headless=new', '--window-size=1920,1080');

  const driver = await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .build();

  try {
    await driver.get('https://example.com/login');

    const email = await driver.wait(until.elementLocated(By.id('email')), 10000);
    await email.sendKeys('jane@example.com');
    await driver.findElement(By.id('password')).sendKeys('S3cure!pass');
    await driver.findElement(By.css("button[type='submit']")).click();

    const heading = await driver.wait(until.elementLocated(By.css('h1')), 10000);
    const text = await heading.getText();

    if (!text.includes('Welcome')) {
      throw new Error(`Unexpected heading: ${text}`);
    }
    console.log('Sign-in test passed');
  } finally {
    await driver.quit();
  }
})();
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

(async function userCanSignIn() {
  const options = new chrome.Options()
    .addArguments('--headless=new', '--window-size=1920,1080');

  const driver = await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .build();

  try {
    await driver.get('https://example.com/login');

    const email = await driver.wait(until.elementLocated(By.id('email')), 10000);
    await email.sendKeys('jane@example.com');
    await driver.findElement(By.id('password')).sendKeys('S3cure!pass');
    await driver.findElement(By.css("button[type='submit']")).click();

    const heading = await driver.wait(until.elementLocated(By.css('h1')), 10000);
    const text = await heading.getText();

    if (!text.includes('Welcome')) {
      throw new Error(`Unexpected heading: ${text}`);
    }
    console.log('Sign-in test passed');
  } finally {
    await driver.quit();
  }
})();
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

(async function userCanSignIn() {
  const options = new chrome.Options()
    .addArguments('--headless=new', '--window-size=1920,1080');

  const driver = await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .build();

  try {
    await driver.get('https://example.com/login');

    const email = await driver.wait(until.elementLocated(By.id('email')), 10000);
    await email.sendKeys('jane@example.com');
    await driver.findElement(By.id('password')).sendKeys('S3cure!pass');
    await driver.findElement(By.css("button[type='submit']")).click();

    const heading = await driver.wait(until.elementLocated(By.css('h1')), 10000);
    const text = await heading.getText();

    if (!text.includes('Welcome')) {
      throw new Error(`Unexpected heading: ${text}`);
    }
    console.log('Sign-in test passed');
  } finally {
    await driver.quit();
  }
})();
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

(async function userCanSignIn() {
  const options = new chrome.Options()
    .addArguments('--headless=new', '--window-size=1920,1080');

  const driver = await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .build();

  try {
    await driver.get('https://example.com/login');

    const email = await driver.wait(until.elementLocated(By.id('email')), 10000);
    await email.sendKeys('jane@example.com');
    await driver.findElement(By.id('password')).sendKeys('S3cure!pass');
    await driver.findElement(By.css("button[type='submit']")).click();

    const heading = await driver.wait(until.elementLocated(By.css('h1')), 10000);
    const text = await heading.getText();

    if (!text.includes('Welcome')) {
      throw new Error(`Unexpected heading: ${text}`);
    }
    console.log('Sign-in test passed');
  } finally {
    await driver.quit();
  }
})();

‍3. C# Implementation

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

[TestFixture]
public class LoginTests
{
    private IWebDriver driver;

    [SetUp]
    public void SetUp()
    {
        var options = new ChromeOptions();
        options.AddArgument("--headless=new");
        options.AddArgument("--window-size=1920,1080");
        driver = new ChromeDriver(options);
    }

    [Test]
    public void UserCanSignIn()
    {
        driver.Navigate().GoToUrl("https://example.com/login");
        var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

        wait.Until(d => d.FindElement(By.Id("email"))).SendKeys("jane@example.com");
        driver.FindElement(By.Id("password")).SendKeys("S3cure!pass");
        driver.FindElement(By.CssSelector("button[type='submit']")).Click();

        var heading = wait.Until(d => d.FindElement(By.TagName("h1")));
        Assert.That(heading.Text, Does.Contain("Welcome"));
    }

    [TearDown]
    public void TearDown()
    {
        driver.Quit

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

[TestFixture]
public class LoginTests
{
    private IWebDriver driver;

    [SetUp]
    public void SetUp()
    {
        var options = new ChromeOptions();
        options.AddArgument("--headless=new");
        options.AddArgument("--window-size=1920,1080");
        driver = new ChromeDriver(options);
    }

    [Test]
    public void UserCanSignIn()
    {
        driver.Navigate().GoToUrl("https://example.com/login");
        var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

        wait.Until(d => d.FindElement(By.Id("email"))).SendKeys("jane@example.com");
        driver.FindElement(By.Id("password")).SendKeys("S3cure!pass");
        driver.FindElement(By.CssSelector("button[type='submit']")).Click();

        var heading = wait.Until(d => d.FindElement(By.TagName("h1")));
        Assert.That(heading.Text, Does.Contain("Welcome"));
    }

    [TearDown]
    public void TearDown()
    {
        driver.Quit

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

[TestFixture]
public class LoginTests
{
    private IWebDriver driver;

    [SetUp]
    public void SetUp()
    {
        var options = new ChromeOptions();
        options.AddArgument("--headless=new");
        options.AddArgument("--window-size=1920,1080");
        driver = new ChromeDriver(options);
    }

    [Test]
    public void UserCanSignIn()
    {
        driver.Navigate().GoToUrl("https://example.com/login");
        var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

        wait.Until(d => d.FindElement(By.Id("email"))).SendKeys("jane@example.com");
        driver.FindElement(By.Id("password")).SendKeys("S3cure!pass");
        driver.FindElement(By.CssSelector("button[type='submit']")).Click();

        var heading = wait.Until(d => d.FindElement(By.TagName("h1")));
        Assert.That(heading.Text, Does.Contain("Welcome"));
    }

    [TearDown]
    public void TearDown()
    {
        driver.Quit

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

[TestFixture]
public class LoginTests
{
    private IWebDriver driver;

    [SetUp]
    public void SetUp()
    {
        var options = new ChromeOptions();
        options.AddArgument("--headless=new");
        options.AddArgument("--window-size=1920,1080");
        driver = new ChromeDriver(options);
    }

    [Test]
    public void UserCanSignIn()
    {
        driver.Navigate().GoToUrl("https://example.com/login");
        var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

        wait.Until(d => d.FindElement(By.Id("email"))).SendKeys("jane@example.com");
        driver.FindElement(By.Id("password")).SendKeys("S3cure!pass");
        driver.FindElement(By.CssSelector("button[type='submit']")).Click();

        var heading = wait.Until(d => d.FindElement(By.TagName("h1")));
        Assert.That(heading.Text, Does.Contain("Welcome"));
    }

    [TearDown]
    public void TearDown()
    {
        driver.Quit

WebDriverWait lives in the Selenium.Support NuGet package, so add it alongside Selenium.WebDriver.

CI/CD Integration Strategies

  1. Jenkins Integration

Jenkinsfile Configuration:

pipeline {
    agent any

    stages {
        stage('Install dependencies') {
            steps {
                sh 'python3 -m venv venv'
                sh '. venv/bin/activate && pip install -r requirements.txt'
            }
        }

        stage('Run headless tests') {
            steps {
                sh '. venv/bin/activate && pytest tests/ --junitxml=results.xml'
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}
pipeline {
    agent any

    stages {
        stage('Install dependencies') {
            steps {
                sh 'python3 -m venv venv'
                sh '. venv/bin/activate && pip install -r requirements.txt'
            }
        }

        stage('Run headless tests') {
            steps {
                sh '. venv/bin/activate && pytest tests/ --junitxml=results.xml'
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}
pipeline {
    agent any

    stages {
        stage('Install dependencies') {
            steps {
                sh 'python3 -m venv venv'
                sh '. venv/bin/activate && pip install -r requirements.txt'
            }
        }

        stage('Run headless tests') {
            steps {
                sh '. venv/bin/activate && pytest tests/ --junitxml=results.xml'
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}
pipeline {
    agent any

    stages {
        stage('Install dependencies') {
            steps {
                sh 'python3 -m venv venv'
                sh '. venv/bin/activate && pip install -r requirements.txt'
            }
        }

        stage('Run headless tests') {
            steps {
                sh '. venv/bin/activate && pytest tests/ --junitxml=results.xml'
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}

The Jenkins agent needs Chrome installed. Selenium Manager takes care of the matching driver.

  1. GitHub Actions Integration

.github/workflows/test.yml:

name: Headless Selenium Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run headless tests
        run: pytest tests/ --junitxml=results.xml

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path

name: Headless Selenium Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run headless tests
        run: pytest tests/ --junitxml=results.xml

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path

name: Headless Selenium Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run headless tests
        run: pytest tests/ --junitxml=results.xml

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path

name: Headless Selenium Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run headless tests
        run: pytest tests/ --junitxml=results.xml

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path

GitHub's Ubuntu runners come with Chrome preinstalled, so there's no browser setup step.

  1. Azure DevOps Integration

azure-pipelines.yml:

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.12'

  - script: pip install -r requirements.txt
    displayName: 'Install dependencies'

  - script: pytest tests/ --junitxml=results.xml
    displayName: 'Run headless tests'

  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFiles: 'results.xml'
      testRunTitle: 'Headless Selenium Tests'
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.12'

  - script: pip install -r requirements.txt
    displayName: 'Install dependencies'

  - script: pytest tests/ --junitxml=results.xml
    displayName: 'Run headless tests'

  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFiles: 'results.xml'
      testRunTitle: 'Headless Selenium Tests'
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.12'

  - script: pip install -r requirements.txt
    displayName: 'Install dependencies'

  - script: pytest tests/ --junitxml=results.xml
    displayName: 'Run headless tests'

  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFiles: 'results.xml'
      testRunTitle: 'Headless Selenium Tests'
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.12'

  - script: pip install -r requirements.txt
    displayName: 'Install dependencies'

  - script: pytest tests/ --junitxml=results.xml
    displayName: 'Run headless tests'

  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFiles: 'results.xml'
      testRunTitle: 'Headless Selenium Tests'

Common Headless Testing Challenges

Challenge 1: Viewport and Resolution Issues

  • ‍Problem: Tests pass in a visible browser but fail headlessly, because the headless browser starts with a smaller default window. Responsive layouts switch to a different breakpoint, and elements move or disappear.‍
    ‍

  • Solution: Set the window size explicitly:

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

# Or set it after the browser starts
driver.set_window_size(1920, 1080)
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

# Or set it after the browser starts
driver.set_window_size(1920, 1080)
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

# Or set it after the browser starts
driver.set_window_size(1920, 1080)
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

# Or set it after the browser starts
driver.set_window_size(1920, 1080)

Challenge 2: Timing and Synchronization

  • ‍Problem: Headless browsers can run faster than visible ones, which exposes race conditions. A test tries to click a button before it's ready.‍
    ‍

  • Solution: Use explicit waits instead of fixed delays:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Avoid: time.sleep(5)
button = wait.until(EC.element_to_be_clickable((By.ID, "place-order")))
button.click()
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Avoid: time.sleep(5)
button = wait.until(EC.element_to_be_clickable((By.ID, "place-order")))
button.click()
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Avoid: time.sleep(5)
button = wait.until(EC.element_to_be_clickable((By.ID, "place-order")))
button.click()
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Avoid: time.sleep(5)
button = wait.until(EC.element_to_be_clickable((By.ID, "place-order")))
button.click()

Challenge 3: Screenshot Verification

  • ‍Problem: Screenshots from headless browsers work, but fonts, anti-aliasing and GPU differences can make them vary slightly from visible runs, and between machines.‍
    ‍

  • Solution: Compare screenshots with a small tolerance rather than requiring an exact match:

from PIL import Image, ImageChops


def images_match(baseline_path, current_path, tolerance=0.01):
    baseline = Image.open(baseline_path).convert("RGB")
    current = Image.open(current_path).convert("RGB")

    if baseline.size != current.size:
        return False

    diff = ImageChops.difference(baseline, current)
    changed_pixels = sum(1 for pixel in diff.getdata() if pixel != (0, 0, 0))
    total_pixels = baseline.width * baseline.height

    return changed_pixels / total_pixels <= tolerance


driver.save_screenshot("current.png")
assert images_match("baseline.png", "current.png")
from PIL import Image, ImageChops


def images_match(baseline_path, current_path, tolerance=0.01):
    baseline = Image.open(baseline_path).convert("RGB")
    current = Image.open(current_path).convert("RGB")

    if baseline.size != current.size:
        return False

    diff = ImageChops.difference(baseline, current)
    changed_pixels = sum(1 for pixel in diff.getdata() if pixel != (0, 0, 0))
    total_pixels = baseline.width * baseline.height

    return changed_pixels / total_pixels <= tolerance


driver.save_screenshot("current.png")
assert images_match("baseline.png", "current.png")
from PIL import Image, ImageChops


def images_match(baseline_path, current_path, tolerance=0.01):
    baseline = Image.open(baseline_path).convert("RGB")
    current = Image.open(current_path).convert("RGB")

    if baseline.size != current.size:
        return False

    diff = ImageChops.difference(baseline, current)
    changed_pixels = sum(1 for pixel in diff.getdata() if pixel != (0, 0, 0))
    total_pixels = baseline.width * baseline.height

    return changed_pixels / total_pixels <= tolerance


driver.save_screenshot("current.png")
assert images_match("baseline.png", "current.png")
from PIL import Image, ImageChops


def images_match(baseline_path, current_path, tolerance=0.01):
    baseline = Image.open(baseline_path).convert("RGB")
    current = Image.open(current_path).convert("RGB")

    if baseline.size != current.size:
        return False

    diff = ImageChops.difference(baseline, current)
    changed_pixels = sum(1 for pixel in diff.getdata() if pixel != (0, 0, 0))
    total_pixels = baseline.width * baseline.height

    return changed_pixels / total_pixels <= tolerance


driver.save_screenshot("current.png")
assert images_match("baseline.png", "current.png")

For anything beyond simple checks, a dedicated visual testing tool will handle rendering noise far better.

Challenge 4: Browser Console Access

  • ‍Problem: Without a visible DevTools window, JavaScript errors in the page are easy to miss.‍
    ‍

  • Solution: Capture console logs in code. In Chrome and Edge, enable browser logging:

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
driver = webdriver.Chrome(options=options)

driver.get("https://example.com")

for entry in driver.get_log("browser"):
    print(entry["level"], entry["message"])
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
driver = webdriver.Chrome(options=options)

driver.get("https://example.com")

for entry in driver.get_log("browser"):
    print(entry["level"], entry["message"])
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
driver = webdriver.Chrome(options=options)

driver.get("https://example.com")

for entry in driver.get_log("browser"):
    print(entry["level"], entry["message"])
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
driver = webdriver.Chrome(options=options)

driver.get("https://example.com")

for entry in driver.get_log("browser"):
    print(entry["level"], entry["message"])

This logging method only works in Chromium-based browsers. For Firefox, use Selenium's WebDriver BiDi support, which listens for console messages across browsers.

Challenge 5: Resource Cleanup

  • ‍Problem: When a test fails partway through, the browser process can be left running. Over many runs, these leftover processes pile up and slow the machine down.‍
    ‍

  • Solution: Always quit the driver in a finally block, or use a test fixture that cleans up for you:

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    # test steps here
finally:
    driver.quit()
driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    # test steps here
finally:
    driver.quit()
driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    # test steps here
finally:
    driver.quit()
driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    # test steps here
finally:
    driver.quit()

Use driver.quit() rather than driver.close(). close() only shuts the current window, while quit() ends the whole browser session.

Limitations of Traditional Headless Testing

  1. Infrastructure Management Overhead

Browser and environment setup

Selenium Manager now handles drivers automatically, which removes one of the oldest headaches. But every CI agent still needs the right browsers installed and kept up to date, along with system dependencies and network access to your test environments. With several agents, that work multiplies.

Cross-platform differences

Headless behaviour can differ slightly between Linux, Windows and macOS, especially with fonts and rendering, so some tests need platform-specific handling.

  1. Limited Parallel Capacity

Single machine limits

Headless browsers are lighter, but not free. A typical CI agent can only run a handful of browsers in parallel before memory or CPU runs out.

Scaling complexity

Going further means adding agents, spreading tests across them, and collecting the results back together, which is infrastructure your team has to build and maintain.

  1. Narrow Browser Coverage

The Chromium default

Most teams default to headless Chrome because it's fast and easy. Real users on Firefox and Safari hit bugs that headless Chrome never shows.

No Safari, no mobile

Safari has no headless mode, and headless desktop browsers don't test mobile behaviour like touch events, mobile layouts or device-specific features.

  1. Harder Debugging

No visual feedback

When a headless test fails, you can't watch what happened. You're working from logs, screenshots and DOM snapshots.

Awkward inspection tools

DevTools is still available, but it's much harder to use against a browser you can't see.

Going Beyond Local Headless Infrastructure

  1. Managed Cloud Execution Grids

Cloud testing platforms remove the need to run your own browsers. They provide managed browser and device environments that your tests connect to over the internet.

Architecture Shift:

Local headless:
Test code → Local WebDriver → Browsers installed on your CI agent
→ Limited by that machine's capacity

Cloud execution:
Test code → Cloud platform → Managed browsers and devices
→ Scales with the platform, not your hardware
Local headless:
Test code → Local WebDriver → Browsers installed on your CI agent
→ Limited by that machine's capacity

Cloud execution:
Test code → Cloud platform → Managed browsers and devices
→ Scales with the platform, not your hardware
Local headless:
Test code → Local WebDriver → Browsers installed on your CI agent
→ Limited by that machine's capacity

Cloud execution:
Test code → Cloud platform → Managed browsers and devices
→ Scales with the platform, not your hardware
Local headless:
Test code → Local WebDriver → Browsers installed on your CI agent
→ Limited by that machine's capacity

Cloud execution:
Test code → Cloud platform → Managed browsers and devices
→ Scales with the platform, not your hardware

For framework-based suites, switching to a cloud grid is often a one-line change, pointing webdriver.Remote at the provider's endpoint:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.set_capability("platformName", "Windows 11")

driver = webdriver.Remote(
    command_executor="https://<your-cloud-grid-endpoint>/wd/hub",
    options=options
)
from selenium import webdriver

options = webdriver.ChromeOptions()
options.set_capability("platformName", "Windows 11")

driver = webdriver.Remote(
    command_executor="https://<your-cloud-grid-endpoint>/wd/hub",
    options=options
)
from selenium import webdriver

options = webdriver.ChromeOptions()
options.set_capability("platformName", "Windows 11")

driver = webdriver.Remote(
    command_executor="https://<your-cloud-grid-endpoint>/wd/hub",
    options=options
)
from selenium import webdriver

options = webdriver.ChromeOptions()
options.set_capability("platformName", "Windows 11")

driver = webdriver.Remote(
    command_executor="https://<your-cloud-grid-endpoint>/wd/hub",
    options=options
)

Key Advantages:

  • No browser setup: no installing, updating or configuring browsers on CI agents

  • Scalable parallel runs: many tests at once, without provisioning more servers

  • Real cross-browser coverage: Chrome, Firefox, Edge and Safari across operating systems

  • Real devices: mobile browsers, tablets and responsive layouts on actual hardware

  1. CI/CD Integration Without Infrastructure

Cloud-native testing platforms connect to your pipeline through official integrations, rather than running browsers on your CI agents. Virtuoso QA, for example, integrates with Jenkins, Azure DevOps and GitHub Actions, and runs tests on a cloud grid covering 2,000+ browser, OS and device combinations.

Supported Integrations:

  • Jenkins (native plugin)

  • Azure DevOps (marketplace extension)

  • GitHub Actions (official action)

  • GitLab CI (Docker image)

  • CircleCI (orb)

  • Bamboo (add-on)

Configuration Example:

# Illustrative only: replace with your platform's official integration
name: Run Cloud Test Suite

on: [push]

jobs:
  cloud-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger test suite on cloud platform
        run: |
          curl -X POST "https://<platform-api>/executions" \
            -H "Authorization: Bearer ${{ secrets.TEST_PLATFORM_TOKEN }}" \
            -d '{"suite": "regression", "browsers": ["chrome", "firefox", "edge"]}'
# Illustrative only: replace with your platform's official integration
name: Run Cloud Test Suite

on: [push]

jobs:
  cloud-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger test suite on cloud platform
        run: |
          curl -X POST "https://<platform-api>/executions" \
            -H "Authorization: Bearer ${{ secrets.TEST_PLATFORM_TOKEN }}" \
            -d '{"suite": "regression", "browsers": ["chrome", "firefox", "edge"]}'
# Illustrative only: replace with your platform's official integration
name: Run Cloud Test Suite

on: [push]

jobs:
  cloud-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger test suite on cloud platform
        run: |
          curl -X POST "https://<platform-api>/executions" \
            -H "Authorization: Bearer ${{ secrets.TEST_PLATFORM_TOKEN }}" \
            -d '{"suite": "regression", "browsers": ["chrome", "firefox", "edge"]}'
# Illustrative only: replace with your platform's official integration
name: Run Cloud Test Suite

on: [push]

jobs:
  cloud-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger test suite on cloud platform
        run: |
          curl -X POST "https://<platform-api>/executions" \
            -H "Authorization: Bearer ${{ secrets.TEST_PLATFORM_TOKEN }}" \
            -d '{"suite": "regression", "browsers": ["chrome", "firefox", "edge"]}'

There's no browser installation or infrastructure to manage on the CI side. The pipeline just asks the platform to run the suite and waits for the result.

  1. Natural Language Tests That Hide Browser Details

Cloud native platforms with natural language authoring abstract away browser-specific configuration entirely:

Traditional Headless Selenium:

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

driver.get("https://example.com/login")
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "email"))
).send_keys("jane@example.com")
driver.find_element(By.ID, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

driver.get("https://example.com/login")
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "email"))
).send_keys("jane@example.com")
driver.find_element(By.ID, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

driver.get("https://example.com/login")
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "email"))
).send_keys("jane@example.com")
driver.find_element(By.ID, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

driver.get("https://example.com/login")
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "email"))
).send_keys("jane@example.com")
driver.find_element(By.ID, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

Natural language equivalent:

Navigate to "https://example.com/login"
Write "jane@example.com" in the "Email" field
Write "S3cure!pass" in the "Password" field
Click "Sign in"
Look for "Welcome back" on the page
Navigate to "https://example.com/login"
Write "jane@example.com" in the "Email" field
Write "S3cure!pass" in the "Password" field
Click "Sign in"
Look for "Welcome back" on the page
Navigate to "https://example.com/login"
Write "jane@example.com" in the "Email" field
Write "S3cure!pass" in the "Password" field
Click "Sign in"
Look for "Welcome back" on the page
Navigate to "https://example.com/login"
Write "jane@example.com" in the "Email" field
Write "S3cure!pass" in the "Password" field
Click "Sign in"
Look for "Welcome back" on the page

The platform handles:

  • Browser selection and configuration

  • Running tests in the cloud for CI/CD

  • Watching runs visually when you need to debug

  • Running the same test across several browsers

  • Running tests in parallel

  1. Self-Healing That Reduces Test Brittleness

Headless or not, traditional Selenium tests break when the application changes. Self-healing platforms reduce that by finding elements through several signals rather than one locator:

  • Multiple identification strategies: labels, text, attributes and position, not a single selector

  • Automatic waiting: no hand-written wait code for each step

  • Detailed element models: structure, context and visual cues combined

  • Reviewable repairs: in Virtuoso QA, every heal is logged, and your team can accept or reject it, so fixes happen fast without anything changing silently

Every run also produces release evidence, recording what was tested and what was repaired. That's useful when someone asks what the pipeline actually proved before a release went out.‍

Best Practices for Cloud Native Testing

  1. Design for Parallel Execution

Structure your suite so tests can run in any order, at the same time.

Anti-Pattern:

# Tests depend on each other's state
def test_create_order(driver):
    ...  # creates order ORD-1001

def test_cancel_order(driver):
    ...  # assumes ORD-1001 from the previous test exists
# Tests depend on each other's state
def test_create_order(driver):
    ...  # creates order ORD-1001

def test_cancel_order(driver):
    ...  # assumes ORD-1001 from the previous test exists
# Tests depend on each other's state
def test_create_order(driver):
    ...  # creates order ORD-1001

def test_cancel_order(driver):
    ...  # assumes ORD-1001 from the previous test exists
# Tests depend on each other's state
def test_create_order(driver):
    ...  # creates order ORD-1001

def test_cancel_order(driver):
    ...  # assumes ORD-1001 from the previous test exists

Best Practice:

import pytest


@pytest.fixture
def new_order(api_client):
    order = api_client.create_order()
    yield order
    api_client.delete_order(order["id"])


def test_cancel_order(driver, new_order):
    driver.get(f"https://example.com/orders/{new_order['id']}")
    ...  # each test creates and cleans up its own data
import pytest


@pytest.fixture
def new_order(api_client):
    order = api_client.create_order()
    yield order
    api_client.delete_order(order["id"])


def test_cancel_order(driver, new_order):
    driver.get(f"https://example.com/orders/{new_order['id']}")
    ...  # each test creates and cleans up its own data
import pytest


@pytest.fixture
def new_order(api_client):
    order = api_client.create_order()
    yield order
    api_client.delete_order(order["id"])


def test_cancel_order(driver, new_order):
    driver.get(f"https://example.com/orders/{new_order['id']}")
    ...  # each test creates and cleans up its own data
import pytest


@pytest.fixture
def new_order(api_client):
    order = api_client.create_order()
    yield order
    api_client.delete_order(order["id"])


def test_cancel_order(driver, new_order):
    driver.get(f"https://example.com/orders/{new_order['id']}")
    ...  # each test creates and cleans up its own data

Independent tests like these can run in parallel safely, locally with pytest -n auto from pytest-xdist, or across a cloud grid.

2. Leverage Built-In Integrations

Prefer your platform's official CI/CD integration over custom scripts.

Avoid:

# Hand-rolled polling script that you have to maintain
EXEC_ID=$(curl -s -X POST "$API/executions" | jq -r '.id')
while true; do
  STATUS=$(curl -s "$API/executions/$EXEC_ID" | jq -r '.status')
  [ "$STATUS" != "running" ] && break
  sleep 30
done
# Hand-rolled polling script that you have to maintain
EXEC_ID=$(curl -s -X POST "$API/executions" | jq -r '.id')
while true; do
  STATUS=$(curl -s "$API/executions/$EXEC_ID" | jq -r '.status')
  [ "$STATUS" != "running" ] && break
  sleep 30
done
# Hand-rolled polling script that you have to maintain
EXEC_ID=$(curl -s -X POST "$API/executions" | jq -r '.id')
while true; do
  STATUS=$(curl -s "$API/executions/$EXEC_ID" | jq -r '.status')
  [ "$STATUS" != "running" ] && break
  sleep 30
done
# Hand-rolled polling script that you have to maintain
EXEC_ID=$(curl -s -X POST "$API/executions" | jq -r '.id')
while true; do
  STATUS=$(curl -s "$API/executions/$EXEC_ID" | jq -r '.status')
  [ "$STATUS" != "running" ] && break
  sleep 30
done

Prefer:

# Official integration: handles triggering, waiting, results and failures
- task: <PlatformOfficialTask>@1
  inputs:
    suite: 'regression'
    failPipelineOnTestFailure: true
# Official integration: handles triggering, waiting, results and failures
- task: <PlatformOfficialTask>@1
  inputs:
    suite: 'regression'
    failPipelineOnTestFailure: true
# Official integration: handles triggering, waiting, results and failures
- task: <PlatformOfficialTask>@1
  inputs:
    suite: 'regression'
    failPipelineOnTestFailure: true
# Official integration: handles triggering, waiting, results and failures
- task: <PlatformOfficialTask>@1
  inputs:
    suite: 'regression'
    failPipelineOnTestFailure: true

Official integrations give you better error handling and result reporting, and someone else maintains them.

3. Monitor Execution Analytics

Most cloud platforms report on:

  • Execution trends over time

  • Flaky tests that pass and fail without code changes

  • Slow tests and bottlenecks

  • Browser-specific failures

  • How your execution capacity is being used

Review these regularly and use them to tidy up the suite.

Related Reads

Frequently Asked Questions

How do I run Selenium tests in headless mode?‍

Can headless browsers run JavaScript?‍

What is the difference between headless Chrome and HTMLUnitDriver?‍

Why do headless tests fail when headed tests pass?‍

How do I debug headless browser tests?‍

Is headless testing faster than regular browser testing?‍

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA