QA How-To
Selenium BiDi Test WebSocket Messages in Python (2026)
Learn selenium bidi test websocket messages python techniques with a preload script, local echo server, frame assertions, timeouts, and pytest fixtures.
22 min read | 2,450 words
TL;DR
Enable BiDi, install a preload script that wraps the browser's native WebSocket constructor, and store sent and received frames in a page-side queue. Poll that queue from pytest and assert parsed message fields. This approach is needed because standard WebDriver BiDi network events do not expose application WebSocket frames in 2026.
Key Takeaways
- WebDriver BiDi transport messages are different from an application's WebSocket frames.
- The 2026 BiDi network module does not provide standard WebSocket frame events.
- A BiDi preload script can wrap window.WebSocket before application code creates a connection.
- Record direction, payload, URL, timestamp, and connection identity for useful assertions.
- Use explicit polling and semantic JSON checks instead of fixed sleeps or raw string equality.
- Keep the local echo server deterministic so failures identify browser behavior, not a public service.
- Remove the preload script and quit the driver in fixtures to prevent cross-test contamination.
The selenium bidi test websocket messages python workflow has one crucial detail: WebDriver BiDi uses a WebSocket as its own command transport, but that does not mean its network module reports the frames exchanged by your application. In 2026, the standard BiDi network events cover requests, responses, authentication, and fetch errors, not application WebSocket frames.
You can still test those messages reliably. Use BiDi to install a preload script before the page's JavaScript runs, wrap the native WebSocket, record outgoing and incoming data, and read that record from Python. This tutorial builds the complete setup with a local echo server and pytest. If you need broader framework structure first, see the Selenium Python framework guide.
The technique stays on standards-based BiDi for early instrumentation and avoids Chrome-only CDP frame events. It also makes the limitation visible, so your suite does not accidentally claim cross-browser support while depending on a vendor protocol.
TL;DR
| Need | Use | Reason |
|---|---|---|
| Instrument before app startup | driver.script.pin(...) |
Selenium implements this with BiDi script.addPreloadScript |
| Observe sent frames | Wrap WebSocket.prototype.send |
The browser API sees the exact application payload |
| Observe received frames | Add a native message listener |
Captures text messages delivered to the page |
| Wait for frames | WebDriverWait polling |
Avoids arbitrary sleeps and exposes timeouts clearly |
| Inspect protocol traffic directly | Browser-specific CDP | Useful in Chromium, but not the portable solution taught here |
The finished test starts a private server on 127.0.0.1, opens a page that sends JSON, waits for the echo, and asserts both directions. No public endpoint or network account is required.
What You Will Build
You will create a small but production-shaped test project that:
- starts a deterministic WebSocket echo server on an operating-system-assigned port;
- enables WebDriver BiDi in Chrome options;
- pins a JavaScript WebSocket recorder before navigation;
- captures
open,sent,received,error, andclosedrecords; - parses text frames and asserts business fields with pytest;
- proves that connection cleanup and negative assertions work.
The final recorder keeps a per-connection numeric ID. That matters when a page opens a notification socket and a chat socket at the same time. Each record also contains the URL, direction, payload type, and a monotonic browser timestamp.
Prerequisites
Use Python 3.12 or 3.13, Selenium 4.43.0, pytest 8.4.2, and websockets 15.0.1. Install current Chrome or Firefox plus its matching browser support. Selenium Manager resolves the driver automatically in ordinary local setups.
Create an empty directory and virtual environment:
mkdir bidi-websocket-test
cd bidi-websocket-test
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install selenium==4.43.0 pytest==8.4.2 websockets==15.0.1
On Windows PowerShell, activate with .venv\Scripts\Activate.ps1. Exact pins make the example reproducible; update them deliberately after reading Selenium release notes. The Python automation framework guide has useful dependency and fixture patterns if this experiment will become a larger suite.
Verify the environment:
python -c "import selenium, pytest, websockets; print(selenium.__version__, pytest.__version__, websockets.__version__)"
Expected output begins with 4.43.0 8.4.2 15.0.1. Also run google-chrome --version or check the browser's About screen.
Step 1: Understand Selenium BiDi Test WebSocket Messages Python Boundaries
Two WebSocket layers exist in this test. The WebDriver BiDi connection carries automation commands and browser events between Selenium and the driver. Your application connection carries domain messages such as chat.message, price.updated, or job.completed. They share RFC 6455 transport terminology but have different endpoints and schemas.
The current WebDriver BiDi specification defines network events such as network.beforeRequestSent, network.responseStarted, network.responseCompleted, and network.fetchError. It does not define webSocketFrameSent or webSocketFrameReceived. Those familiar names belong to Chrome DevTools Protocol. Calling them portable BiDi APIs would be inaccurate.
Our recorder therefore runs inside the page realm. BiDi's preload command installs it before site scripts execute, which closes the race created by calling execute_script after navigation. This is especially important when a single-page app opens its socket during its first module evaluation.
Create a tiny capability check named test_bidi.py:
from selenium import webdriver
def test_bidi_session_is_enabled():
options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
try:
assert driver.capabilities["webSocketUrl"].startswith("ws")
assert driver.script is not None
finally:
driver.quit()
Verify step 1:
pytest -q test_bidi.py
Expect 1 passed. A missing webSocketUrl usually means an old browser, driver, or Selenium package.
Step 2: Start a Deterministic Local WebSocket Server
A public echo service adds DNS, TLS, rate limits, and third-party uptime to a functional test. Use the synchronous API in websockets 15 instead. It gives the test a real protocol handshake while keeping behavior under your control.
Create conftest.py:
from contextlib import contextmanager
from threading import Event, Thread
import pytest
from websockets.sync.server import serve
def echo(connection):
for message in connection:
connection.send(message)
@contextmanager
def running_echo_server():
ready = Event()
state = {}
def run():
with serve(echo, "127.0.0.1", 0) as server:
state["port"] = server.socket.getsockname()[1]
ready.set()
server.serve_forever()
thread = Thread(target=run, daemon=True)
thread.start()
if not ready.wait(timeout=5):
raise RuntimeError("Echo server did not start")
try:
yield f"ws://127.0.0.1:{state['port']}"
finally:
# The daemon thread ends with the test process. A suite-level server can
# retain the server object and call shutdown() for stricter cleanup.
pass
@pytest.fixture(scope="session")
def websocket_url():
with running_echo_server() as url:
yield url
Port 0 asks the operating system for a free port, avoiding collisions in CI. Binding to 127.0.0.1 also avoids exposing the test server to the local network.
Verify step 2 by adding this temporary test, then keep it as a smoke check if server startup is a frequent CI risk:
def test_echo_server_address(websocket_url):
assert websocket_url.startswith("ws://127.0.0.1:")
Run pytest -q. The address assertion should pass immediately. If the fixture times out, inspect endpoint security software and loopback restrictions.
Step 3: Create the BiDi Preload WebSocket Recorder
Add recorder.py. The declaration must be a JavaScript function because BiDi script.addPreloadScript accepts a function declaration, not an arbitrary script body. Selenium's driver.script.pin is the public high-level API that installs it.
WEBSOCKET_RECORDER = r"""() => {
if (globalThis.__qaWsInstalled) return;
globalThis.__qaWsInstalled = true;
globalThis.__qaWsEvents = [];
const NativeWebSocket = globalThis.WebSocket;
let nextId = 1;
function normalize(data) {
if (typeof data === "string") {
return { payloadType: "text", payload: data };
}
if (data instanceof ArrayBuffer) {
return { payloadType: "arraybuffer", byteLength: data.byteLength };
}
if (ArrayBuffer.isView(data)) {
return { payloadType: "typedarray", byteLength: data.byteLength };
}
if (data instanceof Blob) {
return { payloadType: "blob", byteLength: data.size };
}
return { payloadType: typeof data, payload: String(data) };
}
function record(socket, kind, data) {
globalThis.__qaWsEvents.push({
id: socket.__qaWsId,
kind,
url: socket.url,
time: performance.now(),
...normalize(data)
});
}
class ObservedWebSocket extends NativeWebSocket {
constructor(url, protocols) {
super(url, protocols);
this.__qaWsId = nextId++;
this.addEventListener("open", () => record(this, "open", ""));
this.addEventListener("message", event => record(this, "received", event.data));
this.addEventListener("error", () => record(this, "error", ""));
this.addEventListener("close", event => {
record(this, "closed", JSON.stringify({ code: event.code, reason: event.reason }));
});
}
send(data) {
record(this, "sent", data);
return super.send(data);
}
}
Object.defineProperty(ObservedWebSocket, "name", { value: "WebSocket" });
globalThis.WebSocket = ObservedWebSocket;
}"""
Subclassing preserves native constants and normal WebSocket behavior better than returning a proxy-like plain object. The recorder stores only binary lengths because serializing arbitrary blobs synchronously would change timing and memory behavior.
Verify step 3:
python -m py_compile recorder.py conftest.py
No output means Python parsed both modules. JavaScript syntax is exercised after pinning in the next step.
Step 4: Pin the Recorder Before Navigation
Extend conftest.py with a driver fixture. Pinning occurs while the initial blank document is active, and the preload applies to later documents in that browsing context.
from selenium import webdriver
from recorder import WEBSOCKET_RECORDER
@pytest.fixture
def driver():
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.enable_bidi = True
browser = webdriver.Chrome(options=options)
script_id = browser.script.pin(WEBSOCKET_RECORDER)
try:
yield browser
finally:
browser.script.unpin(script_id)
browser.quit()
Do not navigate and then install the wrapper. An application can create and use its socket before Selenium regains control after get(). A preload script also survives normal top-level navigations until you unpin it. It does not retroactively instrument sockets created before installation.
Add this verification to test_bidi.py:
def test_recorder_is_preloaded(driver):
driver.get("data:text/html,<title>recorder-check</title>")
installed = driver.execute_script("return window.__qaWsInstalled")
assert installed is True
Verify step 4 with pytest -q test_bidi.py::test_recorder_is_preloaded. Expect one passing test. If driver.script is unavailable, confirm options.enable_bidi = True was set before driver construction.
Step 5: Build a Page That Sends a Real Message
For a self-contained example, navigate to a percent-encoded data document. The page opens the fixture URL, sends one JSON command after open, renders the echo, and closes normally.
Add these imports and helper to test_bidi.py:
from urllib.parse import quote
def echo_page(socket_url: str) -> str:
html = f"""<!doctype html>
<meta charset="utf-8">
<output id="status">connecting</output>
<script>
const socket = new WebSocket({socket_url!r});
socket.addEventListener("open", () => {{
socket.send(JSON.stringify({{
type: "chat.send",
correlationId: "msg-42",
body: "hello bidi"
}}));
}});
socket.addEventListener("message", event => {{
document.querySelector("#status").textContent = event.data;
socket.close(1000, "test complete");
}});
</script>"""
return "data:text/html;charset=utf-8," + quote(html)
Using repr through {socket_url!r} safely produces a quoted JavaScript string for this controlled local URL. In an application test, navigate to the real UI and trigger the action through elements, not by synthesizing a socket in test code.
Verify step 5 with a UI-level assertion:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def test_page_receives_echo(driver, websocket_url):
driver.get(echo_page(websocket_url))
status = (By.ID, "status")
WebDriverWait(driver, 5).until(
lambda d: "chat.send" in d.find_element(*status).text
)
Run pytest -q test_bidi.py::test_page_receives_echo. The test proves the browser and server exchanged a message before recorder assertions are introduced.
Step 6: Capture and Parse Frames Without Fixed Sleeps
Create helpers that return a fresh copy of the page-side records and wait for a semantic condition. Reading a copy prevents Python from holding a live JavaScript reference.
import json
from selenium.webdriver.support.ui import WebDriverWait
def websocket_events(driver):
return driver.execute_script(
"return structuredClone(window.__qaWsEvents || [])"
)
def wait_for_frame(driver, kind, message_type, timeout=5):
def matching_frame(current_driver):
for event in websocket_events(current_driver):
if event["kind"] != kind or event["payloadType"] != "text":
continue
try:
payload = json.loads(event["payload"])
except json.JSONDecodeError:
continue
if payload.get("type") == message_type:
return event | {"json": payload}
return False
return WebDriverWait(driver, timeout, poll_frequency=0.05).until(matching_frame)
A 50 ms polling interval is responsive enough for a local test without spinning continuously. Keep the timeout explicit. A timeout then communicates that the expected domain event never arrived rather than silently accepting an empty list. For broader synchronization principles, review API error handling and negative testing.
Verify step 6:
def test_capture_helpers(driver, websocket_url):
driver.get(echo_page(websocket_url))
sent = wait_for_frame(driver, "sent", "chat.send")
received = wait_for_frame(driver, "received", "chat.send")
assert sent["json"]["correlationId"] == "msg-42"
assert received["json"]["body"] == "hello bidi"
Run pytest -q test_bidi.py::test_capture_helpers. Both directions should be found even if the echo returns before the first Python poll.
Step 7: Selenium BiDi Test WebSocket Messages Python Assertions
Assert business meaning, ordering, and connection identity separately. A giant equality assertion over the entire event list becomes fragile when the application adds a heartbeat or optional field.
def test_chat_contract_over_websocket(driver, websocket_url):
driver.get(echo_page(websocket_url))
sent = wait_for_frame(driver, "sent", "chat.send")
received = wait_for_frame(driver, "received", "chat.send")
assert sent["json"] == {
"type": "chat.send",
"correlationId": "msg-42",
"body": "hello bidi",
}
assert received["json"]["correlationId"] == sent["json"]["correlationId"]
assert received["id"] == sent["id"]
assert received["url"] == websocket_url + "/"
assert received["time"] >= sent["time"]
events = websocket_events(driver)
errors = [event for event in events if event["kind"] == "error"]
assert errors == []
Browsers may normalize the root WebSocket URL with a trailing slash, so compare against the observed normalized form. performance.now() is suitable for ordering within one document, not for comparing timestamps across machines or navigations.
Verify step 7 with pytest -q test_bidi.py::test_chat_contract_over_websocket -vv. Pytest should report the named contract test as passed. The detailed mode also gives a clean node ID for CI retries.
Step 8: Add Negative, Binary, and Multi-Socket Coverage
Text JSON is the common case, but the recorder deliberately distinguishes binary payloads. For binary messages, assert byte length and validate decoded content through application-visible state or an asynchronous recorder extension. Converting every blob to base64 inside the hook can distort a high-volume stream.
Negative assertions require a bounded observation window. For example, after a user with read-only permissions opens a screen, wait for the stable UI state and then assert that no admin.delete frame was sent:
def assert_no_message_type(driver, forbidden_type):
offenders = []
for event in websocket_events(driver):
if event["kind"] != "sent" or event["payloadType"] != "text":
continue
try:
payload = json.loads(event["payload"])
except json.JSONDecodeError:
continue
if payload.get("type") == forbidden_type:
offenders.append(payload)
assert offenders == [], f"Unexpected {forbidden_type} frames: {offenders}"
With multiple sockets, filter by url and group by id. Do not assume connection ID 1 is always the chat channel, because application startup order can change. For schema-heavy payloads, validate the parsed object against a JSON Schema or Pydantic model. The event-driven API testing guide explains correlation IDs, eventual consistency, and consumer contracts in more depth.
Verify step 8 by adding assert_no_message_type(driver, "admin.delete") after the echo assertion and rerunning pytest -q. Then temporarily change the forbidden type to chat.send; the failure should print the offending payload. Revert that deliberate failure afterward.
Step 9: Make the Test Suite-Ready
Keep instrumentation in a fixture, but keep business assertions in test modules. Clear the queue before each user action when one page hosts several scenarios:
def clear_websocket_events(driver):
driver.execute_script("window.__qaWsEvents = []")
def frames_for_url(driver, expected_url):
return [
event for event in websocket_events(driver)
if event["url"].rstrip("/") == expected_url.rstrip("/")
]
Attach the captured list to failure reports, but redact access tokens, personal data, and message bodies that contain secrets. WebSocket URLs sometimes carry query-string credentials. A generic recorder can turn a harmless test artifact into a sensitive log unless redaction happens before serialization.
Parallel workers need isolated servers and message identities. A stateless echo handler returns data only to its originating connection, but a stateful collaboration service may broadcast across tests. Give every worker a unique room, tenant, user, and correlation ID so one worker cannot satisfy another worker s wait. If you reuse a browser, clear the recorder immediately before the action and confirm the current URL. A full navigation creates a new page global and queue, while a same-document route change retains existing records. Document that lifecycle in the fixture and clean server-side state after each scenario.
Run socket tests in their own pytest marker so CI can choose the appropriate browser and network policy:
# pytest.ini
[pytest]
markers =
websocket: browser tests that exercise WebSocket behavior
import pytest
pytestmark = pytest.mark.websocket
Verify step 9 with pytest --markers and confirm websocket appears. Then run pytest -q -m websocket. For repository organization and ownership boundaries, use test automation repository structure.
Best Practices
- Install the wrapper before navigation. Post-load injection has an unavoidable race with application bootstrap.
- Preserve the native constructor contract. Subclass the browser's
WebSocketand pass both URL and protocols tosuper. - Filter by domain message type and socket URL. Heartbeats, reconnects, and telemetry frames are normal background traffic.
- Parse JSON before comparison. Whitespace and property order do not define JSON meaning.
- Record a connection identity. URL alone cannot distinguish reconnects or two connections to the same endpoint.
- Keep payload retention bounded in long-running pages. Replace the array with a ring buffer or clear it between actions.
- Redact secrets before publishing artifacts. Headers are not captured here, but URLs and payloads can still contain credentials.
- Treat binary data deliberately. Length checks are cheap; full decoding should be enabled only for scenarios that need it.
- Prefer local deterministic services for functional tests. Use staging only when the goal is integration with real infrastructure.
- Keep a UI assertion alongside the frame assertion. A correct message that never updates the interface is still a user-facing defect.
For load and throughput questions, browser functional tests are the wrong layer. Use a protocol load tool such as the approach in k6 WebSocket load testing, then keep Selenium focused on a few critical user journeys.
Troubleshooting
Problem: driver.script is missing or webSocketUrl is absent -> Upgrade Selenium and the browser, set options.enable_bidi = True before constructing the driver, and print driver.capabilities. Do not set a guessed WebSocket URL yourself.
Problem: the queue exists but contains no sent event -> Confirm the preload was pinned before navigation and that the application uses window.WebSocket in the same page realm. A socket created in a worker has a different global scope and needs worker-aware instrumentation.
Problem: the received frame appears before Python starts waiting -> Keep events in a queue as shown. WebDriverWait polls historical records, so it does not require the listener and frame to occur simultaneously.
Problem: JSON decoding fails -> Inspect payloadType and the raw text. Many protocols mix plain-text heartbeats such as PING with JSON messages. Skip unrelated text or branch the parser by protocol message shape.
Problem: the test passes locally but Chrome blocks the connection in CI -> Check mixed-content rules, proxy settings, container loopback, and whether the page is HTTPS while the test endpoint is ws://. Use wss:// with a trusted test certificate when the deployed page requires secure WebSockets.
Problem: duplicate messages appear after reconnects -> Group records by connection id, assert correlation IDs, and check whether the client retransmits unacknowledged commands. Do not deduplicate blindly because the duplicate may be the defect under test.
Interview Questions and Answers
The model answers in the interviewQnA field cover the architectural distinction, timing, portability, binary data, and test design. A strong interview explanation should begin with the current standards limitation, then describe preload instrumentation and semantic assertions. Avoid saying that BiDi network events directly expose application frames.
Where To Go Next
Move the recorder into your browser fixture and apply it to one high-value real-time workflow, such as chat delivery, notification acknowledgment, or live job status. Add URL filtering, domain-specific parsers, and artifact redaction before enabling it across the suite.
Next, strengthen the surrounding framework with the Selenium Python framework guide, study event-driven API testing, and separate browser correctness from WebSocket load testing with k6. Practice explaining the design under interview pressure in the automation testing interview guide, or exercise related automation problems in the QA practice area.
Conclusion
To implement selenium bidi test websocket messages python tests correctly in 2026, use BiDi for early preload instrumentation, not as a nonexistent standard frame-monitoring API. Wrap the native page WebSocket, capture meaningful metadata, poll with an explicit timeout, and compare parsed domain messages.
The local echo example gives you a reproducible baseline. Once it passes, substitute your application URL and user action while retaining the same recorder and helper contract. You will then have assertions that are portable, explainable, and focused on the behavior users depend on.
Interview Questions and Answers
How would you test WebSocket messages with Selenium BiDi in Python?
I would enable BiDi and install a preload script that wraps the native page WebSocket before application code runs. The wrapper records sent and received messages with URL, direction, timestamp, and connection ID. Python then polls the record with an explicit timeout and asserts parsed domain fields such as type and correlation ID.
Why can you not simply subscribe to a BiDi WebSocket frame event?
The 2026 WebDriver BiDi network module does not standardize application WebSocket frame-sent and frame-received events. Similar event names exist in Chrome DevTools Protocol, which is vendor-specific. I would state that boundary explicitly rather than presenting CDP as portable BiDi.
What race condition does a preload script solve?
A single-page application may create its socket during its first script evaluation. Injecting a wrapper after `driver.get` can miss the connection and its earliest messages. A preload script is registered before navigation and executes in the new document before normal application scripts.
How do you make WebSocket message assertions resilient?
I parse structured payloads and assert stable business fields instead of comparing raw JSON strings or the whole traffic list. I filter by socket URL, direction, message type, and correlation ID. I also preserve connection identity so reconnect behavior can be analyzed rather than hidden.
How would you test that a forbidden message was not sent?
I first wait for a positive completion signal that defines the observation window, such as a stable UI state or expected response. Then I inspect all captured outgoing text frames, parse valid JSON, and assert that none has the forbidden type. A bare immediate absence check is weak because the message could be sent later.
What are the limitations of wrapping window.WebSocket?
It changes a page global, can miss sockets created in workers or isolated realms, and must preserve native constructor behavior carefully. It also observes application-level data rather than low-level wire details such as compression and individual protocol frames. For Chromium-only wire inspection, CDP may be appropriate with a documented portability trade-off.
How would you handle binary WebSocket payloads in a browser test?
I would record whether the value is a Blob, ArrayBuffer, or typed array plus its byte length. Full decoding should be asynchronous and scenario-specific because converting every payload can affect the application under test. Often the better end-to-end assertion is the UI state created by that binary message.
How do functional and load tests for WebSockets differ?
A Selenium functional test validates that a real browser action sends the right message and renders the right response. A protocol load test creates many connections and measures capacity, latency, errors, and recovery without browser overhead. I keep those responsibilities separate and share only message schemas and test data where useful.
Frequently Asked Questions
Can Selenium WebDriver BiDi capture WebSocket frames directly in Python?
Not through the standard BiDi network events as of 2026. Those events cover HTTP-style request and response lifecycle data, not application WebSocket frames. Use a BiDi preload script to wrap the page WebSocket API, or use Chromium-specific CDP when portability is not required.
Why use a preload script instead of Selenium execute_script after loading the page?
An application can open and use a WebSocket during startup before navigation returns control to Selenium. A BiDi preload script runs in each new document before ordinary page scripts, preventing that capture race.
Does this technique work in Chrome and Firefox?
The design uses the standard BiDi preload-script capability and the standard browser WebSocket API, so it is intended to be cross-browser where the browser and driver implement those features. Run a capability smoke test for every browser version in your CI matrix because implementation maturity can differ.
How should I assert binary WebSocket messages?
Start by recording payload type and byte length without synchronously transforming large blobs. If content validation is essential, add an asynchronous decoder or assert the application state produced by the binary message. Keep decoding opt-in to avoid altering timing and memory usage.
How do I avoid flaky WebSocket tests in Selenium?
Install instrumentation before navigation, retain frames in a queue, and poll for a semantic condition with a bounded timeout. Use correlation IDs and parsed JSON fields instead of sleeps, list position, or exact raw-string formatting.
Should Selenium be used for WebSocket load testing?
No. Selenium is appropriate for a few end-to-end browser journeys that prove UI and message behavior together. Use a protocol-level tool such as k6 for connection scale, throughput, soak tests, and latency distributions.
What is the difference between the BiDi WebSocket and my application's WebSocket?
The BiDi WebSocket connects the Selenium client to the browser automation endpoint and carries commands, responses, and events. The application WebSocket connects page JavaScript to the application's server and carries business messages. Observing one does not automatically expose the other.
Related Guides
- Selenium DevTools in Selenium 4 in Python (2026)
- How to Debug a failing test in VS Code in Selenium (2026)
- How to Use Selenium DevTools in Selenium 4 in Java (2026)
- Selenium BiDi network in Python (2026)
- How to Debug a failing test in VS Code in Cypress (2026)
- How to Debug a failing test in VS Code in Playwright (2026)