Resource library

QA How-To

Playwright WebSocket Testing Complete Guide (2026)

Playwright WebSocket testing complete guide 2026: inspect frames, mock servers, verify reconnects, and test real-time UI behavior reliably with TypeScript.

20 min read | 2,920 words

TL;DR

Playwright can observe WebSocket creation, sent frames, received frames, socket errors, and closure. It can also intercept WebSocket connections with page.routeWebSocket(), letting you emulate server messages, record client messages, close connections, and test the UI deterministically.

Key Takeaways

  • Observe browser WebSocket traffic with page.on('websocket') and frame event listeners.
  • Use page.routeWebSocket() before navigation to mock a WebSocket endpoint without a separate backend.
  • Assert visible application behavior instead of treating frame inspection as the final result.
  • Test reconnects by closing mocked sockets and verifying both recovery and duplicate-message protection.
  • Control timing with web-first assertions and explicit application signals, not arbitrary sleeps.
  • Separate protocol checks, browser integration checks, and UI journey checks for faster diagnosis.
  • Sanitize frame logs because real-time payloads can contain credentials or personal data.

A practical playwright websocket testing complete guide 2026 must cover more than confirming that a socket opened. A useful test proves what the user experiences when frames arrive, messages leave the browser, the connection fails, and the application reconnects. Playwright gives you browser-level observation through the WebSocket event API and deterministic interception through page.routeWebSocket().

This guide builds a small real-time notification page and a complete TypeScript test suite around it. You will inspect frames, mock server behavior, assert user-visible state, simulate disconnects, and make failures diagnosable. The examples use Playwright Test and APIs available in current Playwright releases.

The central rule is simple: inspect the transport to diagnose behavior, but assert the product outcome to verify behavior. A received JSON frame matters because it changes a notification badge, order status, chat transcript, or dashboard value.

TL;DR

Goal Playwright API Best assertion
Observe a connection page.on('websocket') Expected URL was opened
Inspect traffic framesent and framereceived Parsed frame has required fields
Mock a server page.routeWebSocket() UI reflects the injected message
Read client messages ws.onMessage() Client sends the expected command
Simulate failure ws.close() UI shows offline, then recovers
Catch protocol errors socketerror and close Useful diagnostic is attached

Register listeners and routes before the action that creates the socket, usually before page.goto(). Parse JSON payloads before comparing them. Use Playwright locators and web-first assertions for UI results, and avoid fixed delays.

What You Will Build

By the end of this guide, you will have:

  • A tiny browser application that connects to a WebSocket endpoint and displays connection state, notifications, and acknowledgments.
  • An observation test that records connection URLs and incoming and outgoing frames.
  • A mocked WebSocket test that runs without a live real-time backend.
  • A reconnection test that forces a disconnect and verifies recovery.
  • Reusable helpers for waiting on parsed frames and reporting sanitized diagnostics.

The sample is intentionally small. The same techniques apply to chat, collaborative editing, trading screens, job status monitors, multiplayer features, and any UI driven by persistent bidirectional connections.

Prerequisites

Use Node.js 20 or newer and a current Playwright Test release. Create an empty project and install the runner:

mkdir playwright-ws-guide
cd playwright-ws-guide
npm init -y
npm install -D @playwright/test@latest
npx playwright install chromium

Add these scripts to package.json if you want short commands:

{
  "scripts": {
    "test": "playwright test",
    "test:ws": "playwright test tests/websocket.spec.ts --project=chromium"
  }
}

You should be comfortable with TypeScript, async functions, JSON, locators, and basic WebSocket lifecycle events. You do not need a separate WebSocket package for the mocked tests because Playwright controls the browser connection directly. For deeper test organization, see the Playwright testing complete guide.

Verify the installation with npx playwright --version. Then run npx playwright test --list; an empty project may list no tests, but the command should load without a missing-browser or module error.

Step 1: Create the Playwright WebSocket Testing Complete Guide 2026 Project

Create playwright.config.ts so each test starts a local static server. The server command will use a small Node script added in the next step.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: { timeout: 5_000 },
  use: {
    baseURL: 'http://127.0.0.1:4173',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure'
  },
  webServer: {
    command: 'node server.mjs',
    url: 'http://127.0.0.1:4173',
    reuseExistingServer: !process.env.CI
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } }
  ]
});

The suite begins with Chromium to keep the tutorial focused. Once the behavior is stable, add Firefox and WebKit projects. A browser matrix is valuable, but it should not multiply nondeterministic backend dependencies. Mocked browser integration tests are a good first cross-browser layer.

Verify the step: run npx playwright test --list. Playwright should read the configuration and start looking under tests. If it reports that server.mjs is missing only when a test runs, continue to Step 2.

Step 2: Build a Minimal Real-Time Page

Create server.mjs. It serves one HTML document and deliberately points the browser at ws://127.0.0.1:4173/ws. Playwright will intercept that URL, so this tutorial needs no real socket server.

import http from 'node:http';

const html = `<!doctype html>
<html lang="en">
  <body>
    <h1>Notification Center</h1>
    <p data-testid="status">connecting</p>
    <span data-testid="count">0</span>
    <ul data-testid="notifications"></ul>
    <button data-testid="subscribe">Subscribe</button>
    <script>
      const status = document.querySelector('[data-testid=status]');
      const count = document.querySelector('[data-testid=count]');
      const list = document.querySelector('[data-testid=notifications]');
      let socket;
      let retry;
      let seen = new Set();

      function connect() {
        status.textContent = 'connecting';
        socket = new WebSocket('ws://127.0.0.1:4173/ws');
        socket.addEventListener('open', () => {
          status.textContent = 'online';
          socket.send(JSON.stringify({ type: 'hello', client: 'web' }));
        });
        socket.addEventListener('message', event => {
          const message = JSON.parse(event.data);
          if (message.type === 'notification' && !seen.has(message.id)) {
            seen.add(message.id);
            const item = document.createElement('li');
            item.textContent = message.text;
            list.appendChild(item);
            count.textContent = String(seen.size);
          }
          if (message.type === 'subscribed') status.textContent = 'subscribed';
        });
        socket.addEventListener('close', () => {
          status.textContent = 'offline';
          clearTimeout(retry);
          retry = setTimeout(connect, 100);
        });
        socket.addEventListener('error', () => {
          status.textContent = 'error';
        });
      }

      document.querySelector('[data-testid=subscribe]').addEventListener('click', () => {
        socket.send(JSON.stringify({ type: 'subscribe', channel: 'jobs' }));
      });
      connect();
    </script>
  </body>
</html>`;

http.createServer((request, response) => {
  response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
  response.end(html);
}).listen(4173, '127.0.0.1');

This page models the important states: connecting, online, subscribed, offline, and error. It deduplicates notifications by ID, which becomes important when reconnects or server retries replay data. Production code should use capped exponential backoff with jitter instead of a fixed 100 ms retry. The short interval keeps this isolated example fast.

Verify the step: run node server.mjs, visit http://127.0.0.1:4173, and confirm the heading appears. The status will move to offline or error because no real server accepts the WebSocket. Stop the server before Playwright starts it automatically.

Step 3: Inspect Connections and Frames

Before mocking, learn the observation API. Create tests/websocket.spec.ts with a frame collector. The route supplies deterministic traffic while the page-level listener records what the browser sees.

import { test, expect, type Page } from '@playwright/test';

type Frame = { direction: 'sent' | 'received'; payload: string | Buffer };

function observeWebSockets(page: Page) {
  const urls: string[] = [];
  const frames: Frame[] = [];
  const errors: string[] = [];

  page.on('websocket', socket => {
    urls.push(socket.url());
    socket.on('framesent', event => {
      frames.push({ direction: 'sent', payload: event.payload });
    });
    socket.on('framereceived', event => {
      frames.push({ direction: 'received', payload: event.payload });
    });
    socket.on('socketerror', error => errors.push(error));
  });

  return { urls, frames, errors };
}

test('observes the connection and frames', async ({ page }) => {
  const traffic = observeWebSockets(page);

  await page.routeWebSocket('**/ws', ws => {
    ws.onMessage(message => {
      const parsed = JSON.parse(String(message));
      if (parsed.type === 'hello') {
        ws.send(JSON.stringify({ type: 'notification', id: 'n-1', text: 'Build passed' }));
      }
    });
  });

  await page.goto('/');
  await expect(page.getByText('Build passed')).toBeVisible();

  expect(traffic.urls).toContain('ws://127.0.0.1:4173/ws');
  expect(traffic.frames.some(frame => frame.direction === 'sent')).toBe(true);
  expect(traffic.frames.some(frame => frame.direction === 'received')).toBe(true);
  expect(traffic.errors).toEqual([]);
});

page.on('websocket') fires when the page creates a WebSocket. The framesent and framereceived events expose payloads as strings or buffers. Convert deliberately rather than assuming every frame is JSON text. Binary protocols require schema-aware decoding. The socketerror listener is diagnostic evidence, not a replacement for a product assertion.

Register the observer before page.goto(). Otherwise, a socket created during application startup may escape the listener. For a detailed inspection workflow, use inspect Playwright WebSocket frames step by step.

Verify the step: run npm run test:ws. The test should pass, and the page should display Build passed. Temporarily change n-1 to an invalid shape to confirm the visible assertion catches the contract problem.

Step 4: Mock Server Responses with page.routeWebSocket()

A route handler can act as the server endpoint. Register it before navigation, listen for client messages, and send responses that match the real protocol. Add this test to the same file:

test('subscribes and renders a mocked notification', async ({ page }) => {
  const clientMessages: unknown[] = [];

  await page.routeWebSocket('**/ws', ws => {
    ws.onMessage(message => {
      const parsed = JSON.parse(String(message));
      clientMessages.push(parsed);

      if (parsed.type === 'subscribe' && parsed.channel === 'jobs') {
        ws.send(JSON.stringify({ type: 'subscribed', channel: 'jobs' }));
        ws.send(JSON.stringify({
          type: 'notification',
          id: 'job-42',
          text: 'Regression run completed'
        }));
      }
    });
  });

  await page.goto('/');
  await expect(page.getByTestId('status')).toHaveText('online');
  await page.getByTestId('subscribe').click();

  await expect(page.getByTestId('status')).toHaveText('subscribed');
  await expect(page.getByTestId('notifications'))
    .toContainText('Regression run completed');
  await expect(page.getByTestId('count')).toHaveText('1');
  expect(clientMessages).toContainEqual({ type: 'subscribe', channel: 'jobs' });
});

This test checks both directions. It verifies the command sent by the browser, then verifies the UI produced by server frames. The user-visible assertions are more valuable than an assertion that ws.send() ran because they cover parsing, state updates, and rendering.

Keep mock messages faithful to an owned schema or recorded fixture. A mock that omits required production fields can let broken client code pass. The dedicated Playwright mock WebSocket server responses tutorial expands this pattern with reusable fixtures and multiple message branches.

Verify the step: run npm run test:ws -- --grep "subscribes". Expect one passing test. Change the mock text and confirm the locator follows the new response without any timing changes.

Step 5: Wait for a Specific Parsed Frame

Direct array assertions can race with asynchronous traffic. Create a promise before the triggering action and resolve it only for the expected frame. Add this helper and test:

import type { WebSocket as PlaywrightWebSocket } from '@playwright/test';

function waitForJsonFrame<T>(
  socket: PlaywrightWebSocket,
  direction: 'framesent' | 'framereceived',
  predicate: (value: T) => boolean
): Promise<T> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('Timed out waiting for frame')), 5_000);
    socket.on(direction, event => {
      try {
        const value = JSON.parse(String(event.payload)) as T;
        if (predicate(value)) {
          clearTimeout(timer);
          resolve(value);
        }
      } catch {
        // Ignore non-JSON frames when this helper targets a JSON protocol.
      }
    });
  });
}

test('sends a channel subscription frame', async ({ page }) => {
  let browserSocket!: PlaywrightWebSocket;
  page.on('websocket', socket => { browserSocket = socket; });
  await page.routeWebSocket('**/ws', () => {});
  await page.goto('/');
  await expect(page.getByTestId('status')).toHaveText('online');

  const subscription = waitForJsonFrame<{ type: string; channel: string }>(
    browserSocket,
    'framesent',
    frame => frame.type === 'subscribe'
  );
  await page.getByTestId('subscribe').click();

  await expect(subscription).resolves.toEqual({ type: 'subscribe', channel: 'jobs' });
});

The promise is created before the click, so a fast frame cannot be missed. The predicate ignores unrelated heartbeat or hello messages. The helper also owns its timeout, producing a targeted error rather than leaving the test pending until the global timeout. In a large suite, include the expected message type in the timeout error.

Verify the step: run the test with --grep "channel subscription". It should pass. Change the predicate to frame.type === 'missing'; the test should fail after about five seconds with Timed out waiting for frame, proving the failure is localized.

Step 6: Test WebSocket Reconnection Logic

Reconnection tests need controlled connection instances. Count route callbacks, close the first socket, and let the page create another one. Then replay a notification ID to verify deduplication.

test('recovers after disconnect without duplicating notifications', async ({ page }) => {
  let connectionCount = 0;

  await page.routeWebSocket('**/ws', ws => {
    connectionCount += 1;
    const current = connectionCount;

    ws.onMessage(message => {
      const parsed = JSON.parse(String(message));
      if (parsed.type !== 'hello') return;

      ws.send(JSON.stringify({
        type: 'notification',
        id: 'stable-id',
        text: 'Deployment ready'
      }));

      if (current === 1) {
        setTimeout(() => ws.close(), 20);
      }
    });
  });

  await page.goto('/');
  await expect(page.getByText('Deployment ready')).toBeVisible();
  await expect(page.getByTestId('status')).toHaveText('offline');
  await expect(page.getByTestId('status')).toHaveText('online');
  await expect.poll(() => connectionCount).toBe(2);
  await expect(page.getByTestId('count')).toHaveText('1');
});

The sequence asserts an observable offline state, recovery to online, a second connection, and no duplicate notification. The mock replays the same ID on purpose. Real services often replay events after a cursor or subscription resumes, so idempotent UI handling deserves a test.

Do not assert exact reconnection milliseconds unless timing is itself a requirement. CI scheduling makes narrow wall-clock windows fragile. Test eventual recovery within the product timeout and test backoff separately at a lower layer with a fake clock if needed. See test Playwright WebSocket reconnection logic for close-code handling, retry limits, and subscription restoration.

Verify the step: run npm run test:ws -- --grep "recovers". The test should pass with connectionCount equal to two and the badge equal to one. Remove seen deduplication from the page to confirm the final assertion fails with a count of two.

Step 7: Assert Real-Time Notifications Accessibly

A transport test becomes useful when it verifies a user outcome. Prefer role, text, label, or stable test ID locators. Inject multiple frames and assert order, count, and content.

test('shows real-time notifications in arrival order', async ({ page }) => {
  await page.routeWebSocket('**/ws', ws => {
    ws.onMessage(message => {
      const parsed = JSON.parse(String(message));
      if (parsed.type === 'hello') {
        ws.send(JSON.stringify({ type: 'notification', id: '1', text: 'Run started' }));
        ws.send(JSON.stringify({ type: 'notification', id: '2', text: 'Run passed' }));
      }
    });
  });

  await page.goto('/');
  const items = page.getByTestId('notifications').getByRole('listitem');

  await expect(items).toHaveCount(2);
  await expect(items).toHaveText(['Run started', 'Run passed']);
  await expect(page.getByTestId('count')).toHaveText('2');
});

toHaveCount, toHaveText, and toContainText retry until the UI reaches the expected state. That makes them safer than reading text once with textContent() and manually comparing it. If the application sorts newest first, encode that product rule in the expected array.

Add checks for unread styling, screen-reader announcements, timestamps, navigation targets, and dismissal only when those are part of the contract. The Playwright real-time notification examples show more UI patterns. For resilient locator design, consult Playwright locator best practices.

Verify the step: run the notification test. It should find exactly two list items in arrival order. Swap the expected strings to verify that the assertion reports a useful ordered diff.

Step 8: Cover Errors, Malformed Frames, and Binary Data

Happy-path JSON is not the whole protocol. Add production-safe handling for invalid payloads, unexpected types, and binary frames, then test the visible fallback. For this sample, capture page errors so malformed JSON is never silently ignored.

test('reports a malformed server frame', async ({ page }) => {
  const pageErrors: string[] = [];
  page.on('pageerror', error => pageErrors.push(error.message));

  await page.routeWebSocket('**/ws', ws => {
    ws.onMessage(message => {
      const parsed = JSON.parse(String(message));
      if (parsed.type === 'hello') ws.send('{not-valid-json');
    });
  });

  await page.goto('/');
  await expect.poll(() => pageErrors.length).toBe(1);
  expect(pageErrors[0]).toContain('JSON');
});

This test documents the current behavior: malformed JSON produces an uncaught page error. A production application should usually catch parsing failures, log a sanitized diagnostic, and keep the stream alive or show a recoverable state. Once that behavior exists, replace the pageerror expectation with the intended UI and telemetry assertions.

For binary frames, event.payload or the routed message can be a Buffer. Do not apply String() unless text decoding is correct for the protocol. Decode with the actual schema, such as Protocol Buffers or a documented byte layout, and keep that decoder in shared production or contract-test code. Avoid inventing a test-only interpretation.

Verify the step: run with --grep "malformed". Expect the test to pass by detecting one parsing error. Change the mock to valid JSON and confirm the polling assertion fails, proving the test is sensitive to malformed content.

Step 9: Add Diagnostics Without Leaking Secrets

Frame logs are helpful, but raw payloads may contain authorization data, personal details, or private messages. Sanitize structured payloads and attach a bounded transcript only on failure.

function sanitize(payload: string | Buffer): unknown {
  if (Buffer.isBuffer(payload)) return { binaryBytes: payload.length };
  try {
    const value = JSON.parse(payload);
    if (value.token) value.token = '[redacted]';
    if (value.email) value.email = '[redacted]';
    return value;
  } catch {
    return payload.slice(0, 200);
  }
}

test('attaches sanitized socket traffic', async ({ page }, testInfo) => {
  const transcript: unknown[] = [];
  page.on('websocket', socket => {
    socket.on('framesent', event => transcript.push({ sent: sanitize(event.payload) }));
    socket.on('framereceived', event => transcript.push({ received: sanitize(event.payload) }));
  });

  await page.routeWebSocket('**/ws', ws => {
    ws.onMessage(() => ws.send(JSON.stringify({
      type: 'notification', id: 'safe-1', text: 'Profile updated'
    })));
  });
  await page.goto('/');
  await expect(page.getByText('Profile updated')).toBeVisible();

  await testInfo.attach('websocket-transcript', {
    body: Buffer.from(JSON.stringify(transcript.slice(-50), null, 2)),
    contentType: 'application/json'
  });
});

Centralize redaction for your schema. Field-name filtering is only a starting point because secrets can appear in nested structures or unstructured strings. Bound the transcript by frame count and payload length so a busy stream does not exhaust memory or overwhelm a report. Traces, screenshots, console messages, and network evidence together usually explain whether the failure sits in transport, parsing, state, or rendering.

Verify the step: run the test, then open the HTML report with npx playwright show-report. The test attachment should contain sent and received objects, with a maximum of 50 frames. Add a token property locally and confirm the attachment shows [redacted].

Designing a Reliable Test Strategy

Do not put every WebSocket requirement into browser end-to-end tests. Use layers with distinct jobs:

Layer What it proves Typical scope
Protocol or unit Encoding, decoding, reducers, retry policy Many fast cases
Contract Client and server agree on message schemas Each message type
Browser integration with route Frames drive correct application state and UI Main branches and failures
End to end with real backend Deployment, authentication, proxying, and one critical journey work together Few high-value paths

Mocked routes give speed and deterministic edge cases. Real-backend tests reveal proxy configuration, cookies, authentication, TLS, load balancers, and deployment issues that mocks cannot. Keep a small real integration smoke suite and a broader mocked behavior suite. API setup can reduce browser work, as described in Playwright API testing patterns.

Name tests after product behavior, such as restores job subscription after reconnect, rather than implementation details such as websocket test 3. Tag or group them so CI can run mocked tests on every change and real-environment tests where stable infrastructure exists.

Troubleshooting

The route never intercepts the socket -> Register page.routeWebSocket() before page.goto() or before the click that opens the connection. Confirm the glob matches the full WebSocket URL, including its path.

The WebSocket event listener sees no frames -> Attach page.on('websocket') before the socket is created, then attach frame listeners inside that callback immediately. Check whether the application uses WebTransport, server-sent events, or polling instead.

The test passes locally but times out in CI -> Remove arbitrary waits, use locator assertions, and wait for an explicit application state. Confirm the CI proxy permits WebSocket upgrades if the test uses a real backend.

JSON parsing throws for some frames -> Inspect whether the protocol includes heartbeats, plain text, or binary messages. Branch by payload type and message envelope before decoding.

Reconnect creates duplicate messages -> Deduplicate by a stable event ID and test a replayed message after reconnection. Also verify that old subscriptions and listeners are disposed.

The mocked test drifts from production -> Validate fixtures against the owned schema and keep a small real-backend contract or smoke suite. Review protocol changes with both client and service owners.

Common Mistakes and Best Practices

  • Do register observers and routes before the connection starts. Do not add them after navigation and assume startup traffic will wait.
  • Do assert the visible state caused by a frame. Do not stop at counting transport events.
  • Do parse and compare meaningful fields. Do not compare large serialized JSON strings when property order is irrelevant.
  • Do wait for a targeted message or web-first UI state. Do not use waitForTimeout() to guess when the stream is ready.
  • Do cover closure, malformed data, replay, and reconnection. Do not test only the first successful notification.
  • Do preserve one or two real service journeys. Do not assume interception validates authentication, infrastructure, or gateway upgrades.
  • Do redact and bound diagnostics. Do not attach unlimited raw customer traffic to CI artifacts.
  • Do keep message fixtures readable and schema-aligned. Do not bury protocol intent in giant opaque payloads.

The Complete Series

Where To Go Next

Start with the sub-guide that matches your current risk. If failures are opaque, inspect frames. If the service is unstable or hard to seed, mock responses. If users report stale screens, focus on reconnection. If transport works but UI behavior breaks, expand notification assertions.

Then add one real-environment smoke test for the highest-value journey. Run the broad mocked suite on pull requests and the real integration suite in an environment where authentication, proxying, and test data are controlled. This combination gives fast feedback without losing deployment confidence.

Playwright WebSocket Testing Complete Guide 2026 Interview Questions and Answers

Q: How do you inspect WebSocket messages in Playwright?

Listen for the page websocket event, then subscribe to framesent and framereceived on each socket. Parse payloads according to the real protocol and register listeners before the connection begins. Use frame evidence for diagnosis, then assert the resulting application behavior.

Q: How do you mock a WebSocket server in Playwright?

Call page.routeWebSocket() before navigation with a URL pattern and handler. Inside the handler, use ws.onMessage() to read client messages and ws.send() to provide server messages. Use ws.close() when the scenario requires a disconnect.

Q: Why are UI assertions still necessary when frame assertions pass?

A valid frame can be dropped by a parser, reducer, state store, or component. UI assertions prove the user received the intended value. Frame assertions narrow the cause when that outcome fails.

Q: How should a reconnection test avoid flakiness?

Control closure through a mocked route, assert explicit offline and online states, and use retrying assertions such as expect.poll. Avoid narrow elapsed-time assertions and fixed sleeps. Verify restored subscriptions and duplicate protection as separate outcomes.

Q: What can mocked WebSocket tests not prove?

They cannot fully prove production authentication, TLS, reverse-proxy upgrades, load balancer behavior, or service deployment compatibility. Keep a small real-backend smoke or contract layer for those risks.

Q: How do you test binary WebSocket frames?

Treat payloads as buffers and decode them with the real protocol schema. Assert semantic fields after decoding. Never convert arbitrary bytes to text merely to make an assertion convenient.

Q: What is the biggest observability risk in frame logging?

Payloads can expose tokens, personal data, or private content. Redact based on the owned schema, limit stored frames and payload size, and restrict artifact retention and access.

Conclusion

Effective Playwright WebSocket testing combines transport visibility with deterministic control and user-centered assertions. Observe sockets and frames to understand the protocol, use page.routeWebSocket() to exercise hard-to-create states, and prove that the UI handles messages, errors, replays, and reconnects correctly.

Treat each test as evidence for a specific risk. A frame-focused check protects the message contract, a routed scenario protects browser behavior under controlled conditions, and a real-environment smoke test protects the deployment boundary. When a failure occurs, this separation tells you where to investigate first.

Build the mocked happy path first, add disconnection and malformed-data cases, then preserve a small real-backend smoke suite. That layered approach gives fast, explainable tests while still covering the infrastructure that browser mocks cannot reproduce.

Interview Questions and Answers

How does Playwright observe WebSocket traffic?

Playwright emits a websocket event from Page when the browser creates a connection. The resulting WebSocket object emits framesent, framereceived, socketerror, and close events. Listeners must be registered before the connection begins, and payloads must be decoded according to the protocol.

How would you mock WebSocket behavior in a Playwright test?

I would register page.routeWebSocket() before navigation and match only the intended endpoint. In the handler, I would inspect client messages through ws.onMessage(), send schema-valid responses through ws.send(), and close the route for failure scenarios. I would primarily assert the resulting UI state.

How do you design a reliable WebSocket reconnection test?

I control the first connection, close it deliberately, and record subsequent route callbacks. I assert an offline state, eventual recovery, restored subscriptions, and correct processing of replayed events. I avoid fixed sleeps and overly precise timing checks.

What is the difference between frame assertions and UI assertions?

Frame assertions prove that a protocol message crossed the connection with expected content. UI assertions prove that parsing, state management, and rendering produced the user outcome. I use UI assertions for acceptance and frames for targeted contract checks and diagnosis.

What risks remain after all mocked WebSocket tests pass?

Mocked tests do not fully cover authentication handshakes, cookies, TLS, proxy upgrades, load balancers, deployment configuration, or real server schema drift. I retain a small set of real-environment smoke and contract checks for those boundaries.

How do you prevent flaky waits in real-time Playwright tests?

I create event promises before the triggering action and filter for a specific semantic message. For UI behavior, I use Playwright's retrying locator assertions and expect.poll. I avoid waitForTimeout because it guesses instead of observing readiness.

How would you handle WebSocket test diagnostics securely?

I sanitize frames using schema-aware redaction, limit transcript length and payload size, and attach evidence only where needed. Binary data is represented by safe metadata unless decoding is required. Artifact access and retention should follow the sensitivity of the underlying messages.

Frequently Asked Questions

Can Playwright test WebSocket connections?

Yes. Playwright can observe socket creation, sent and received frames, socket errors, and closure. It can also intercept connections with page.routeWebSocket() so tests can emulate server behavior.

How do I inspect WebSocket frames in Playwright?

Register page.on('websocket') before the connection starts. On each socket, listen for framesent and framereceived, then decode each payload according to the application protocol.

Can Playwright mock a WebSocket server without another package?

Yes. Register page.routeWebSocket() before navigation, read browser messages with ws.onMessage(), and send mocked server messages with ws.send(). A separate server is unnecessary for browser integration scenarios.

How do I test WebSocket reconnection in Playwright?

Close the first routed connection with ws.close(), allow the application to retry, and count new route callbacks. Assert the offline and recovered UI states, restored subscriptions, and duplicate-message protection.

Should WebSocket tests assert frames or the UI?

Use both for different purposes. Frame assertions validate protocol details and improve diagnosis, while UI assertions prove the user-visible result and should usually be the primary end-to-end outcome.

Why does my Playwright WebSocket test miss early messages?

The observer or route was probably registered after the application created its socket. Install page listeners and routeWebSocket handlers before page.goto() or before the action that opens the connection.

Does a mocked WebSocket test replace a real backend test?

No. A mock gives deterministic application behavior but does not prove authentication, TLS, proxies, deployment, or server compatibility. Keep a small real-environment smoke or contract suite.

Related Guides