Realtime Testing Guide

How to Test Pusher Realtime with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Pusher realtime features with Playwright. Channel authentication for private and presence channels, WebSocket transport fallback, presence member tracking, client events, and the connection state machine that silently breaks test suites.

800K+

Pusher powers realtime features for over 800,000 developers and handles billions of messages daily across chat, notifications, live dashboards, and collaboration tools.

Pusher

0Connection states to handle
0Scenarios covered
0xAuth endpoints per channel type
0%Fewer lines with Assrt

Pusher Realtime Message Flow

BrowserYour App ServerPusherOther ClientsWebSocket connectconnection_established (socket_id)POST /pusher/auth (private channel)Auth signaturepusher:subscribe (private-room)subscription_succeededTrigger event on channelEvent payload delivered

1. Why Testing Pusher Realtime Is Harder Than It Looks

Pusher adds a persistent WebSocket connection between the browser and Pusher's cluster servers. Unlike HTTP request/response cycles that Playwright intercepts cleanly with page.route(), WebSocket frames are a separate transport that lives outside the fetch API. Your test cannot simply mock a Pusher message by intercepting a network request. Instead, you need to either trigger real server-side events through your backend API or intercept WebSocket frames at the protocol level using Playwright's page.on('websocket') listener.

The complexity escalates with channel types. Public channels require no authentication, so subscribing is straightforward. Private channels require a server-side auth endpoint that validates the user's session and returns a signed token. Presence channels add member tracking on top of private channel auth, requiring your auth endpoint to return both a signature and user info. Each channel type has a different subscription handshake, and a misconfigured auth endpoint produces silent failures where the subscription simply never succeeds.

Then there is the connection state machine. Pusher's client library manages seven distinct states: initialized, connecting, connected, unavailable, failed, disconnected, and reconnecting. Each transition fires a callback, and your application likely binds UI updates to these states (showing a “reconnecting” banner, disabling send buttons, queuing messages). Testing these transitions requires you to simulate network interruptions, which means dropping the WebSocket connection mid-test and verifying the UI responds correctly.

Client events add another layer. On channels prefixed with private- or presence-, authenticated clients can trigger events directly to other subscribers without going through your server. These events are prefixed with client- and bypass your backend entirely, flowing from one browser through Pusher to another browser. Testing this requires two browser contexts subscribed to the same channel simultaneously.

Finally, Pusher supports transport fallback. If WebSockets are blocked (corporate proxies, restrictive firewalls), the client falls back to HTTP streaming or HTTP polling. Your tests should verify this fallback works, because a meaningful percentage of enterprise users will hit it.

Pusher Channel Authentication Flow

🌐

Browser

pusher.subscribe('private-room')

🔒

Pusher Client

Detects private- prefix

↪️

Auth Request

POST /pusher/auth

⚙️

Your Server

Validate session, sign channel

Auth Response

Return signature + channel_data

🔔

Pusher

Subscribe with auth token

Subscribed

subscription_succeeded event

Pusher Connection State Machine

🌐

initialized

new Pusher() created

↪️

connecting

WebSocket handshake

connected

socket_id assigned

unavailable

Network lost

↪️

reconnecting

Auto-retry with backoff

connected

Resubscribes all channels

A comprehensive Pusher test suite must cover all of these surfaces. The sections below walk through each scenario with runnable Playwright TypeScript you can paste directly into your project.

2. Setting Up a Reliable Test Environment

Pusher provides free Sandbox plans that support all channel types including private, presence, and client events. Create a dedicated Pusher app for testing, separate from production. The Sandbox plan allows 200,000 messages per day and 100 concurrent connections, which is more than enough for a test suite.

Pusher Test Environment Checklist

  • Create a dedicated Pusher app on the Sandbox plan (separate from production)
  • Enable client events in the Pusher dashboard under App Settings
  • Note the App ID, Key, Secret, and Cluster from the dashboard
  • Implement the /pusher/auth endpoint in your server for private and presence channels
  • Set up a /api/test/trigger endpoint that fires server-side events for testing
  • Configure CORS to allow your test origin (localhost and CI preview URLs)
  • Install pusher and pusher-js packages in your project
  • Disable Pusher's encrypted channels for simpler test debugging

Environment Variables

.env.test

Server-Side Test Trigger Endpoint

Your Playwright tests need a way to trigger Pusher events from the server side. Create a test-only API endpoint that accepts a channel name, event name, and payload, then calls pusher.trigger(). Protect this endpoint so it only runs in test environments.

app/api/test/trigger/route.ts

Pusher Auth Endpoint

Private and presence channels require a server-side auth endpoint. Pusher's client library sends a POST request to this endpoint with the socket_id and channel_name. Your server validates the user's session, then returns a signed authentication response.

app/api/pusher/auth/route.ts

Playwright Configuration for Pusher Testing

Pusher events are asynchronous and arrive over WebSocket, not via page navigation. Set generous action timeouts and configure your tests to wait for DOM changes that result from received messages rather than waiting for URL changes.

playwright.config.ts
Pusher Test Environment Setup

3. Scenario: Public Channel Subscribe and Receive

The simplest Pusher scenario is subscribing to a public channel and receiving a server-triggered event. Public channels have no authentication requirement, so the subscription succeeds immediately after the WebSocket connection is established. This is your smoke test: if public channel delivery breaks, nothing else works.

1

Public Channel Subscribe and Receive

Straightforward

Goal

Load a page that subscribes to a public channel, trigger a server-side event via the test API, and verify the message appears in the UI within a reasonable timeout.

Preconditions

  • App running at APP_BASE_URL
  • The page subscribes to a public channel on load (e.g., notifications)
  • The test trigger endpoint is available at /api/test/trigger

Playwright Implementation

pusher-public-channel.spec.ts

What to Assert Beyond the UI

  • The WebSocket connection state is connected before triggering
  • The event payload rendered in the DOM matches the triggered data
  • The message appeared within a reasonable window (under 5 seconds for Pusher)

Assrt Equivalent

# scenarios/pusher-public-channel.assrt
describe: Public channel receives server-triggered event

given:
  - I am on the /dashboard page
  - the Pusher connection is established

steps:
  - trigger a "new-message" event on the "notifications" channel
    with text "Hello from Playwright test"

expect:
  - the text "Hello from Playwright test" appears on the page
  - the notification counter is not zero

Public Channel: Playwright vs Assrt

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

test('public channel: receive event', async ({ page }) => {
  await page.goto('/dashboard');

  await page.waitForFunction(() => {
    const w = window as any;
    return w.pusher?.connection?.state === 'connected';
  }, { timeout: 10_000 });

  const triggerRes = await page.request.post('/api/test/trigger', {
    data: {
      channel: 'notifications',
      event: 'new-message',
      data: { text: 'Hello from Playwright test', id: Date.now() },
    },
  });
  expect(triggerRes.ok()).toBe(true);

  await expect(
    page.getByText('Hello from Playwright test')
  ).toBeVisible({ timeout: 10_000 });

  const counter = page.getByTestId('notification-count');
  await expect(counter).not.toHaveText('0');
});
54% fewer lines

4. Scenario: Private Channel Authentication

Private channels are where Pusher testing gets interesting. When the client subscribes to a channel prefixed with private-, the Pusher client library automatically sends a POST request to your auth endpoint with the socket_id and channel_name. Your server must validate the user's session, confirm they have permission to access this channel, and return a signed authentication token. If the auth endpoint returns a non-200 status or malformed response, the subscription fails silently in many Pusher client versions. The channel simply never receives events, and your test hangs until timeout.

2

Private Channel Authentication

Moderate

Goal

As an authenticated user, subscribe to a private channel, verify the auth endpoint is called correctly, receive a server-triggered event, and confirm that unauthenticated users are rejected.

Preconditions

  • User is logged in with a valid session
  • The /api/pusher/auth endpoint is implemented
  • The page subscribes to private-user-{userId} on load

Playwright Implementation

pusher-private-channel.spec.ts

Private Channel Auth: Playwright vs Assrt

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

test('private channel: auth and receive', async ({ page }) => {
  await page.goto('/chat');

  const authRequest = page.waitForRequest(
    (req) => req.url().includes('/api/pusher/auth')
  );
  const req = await authRequest;
  expect(req.postData()).toContain('private-');

  await page.waitForFunction(() => {
    const w = window as any;
    const ch = w.pusher?.channel('private-user-' + w.__userId);
    return ch?.subscribed === true;
  }, { timeout: 10_000 });

  const userId = await page.evaluate(() => (window as any).__userId);
  await page.request.post('/api/test/trigger', {
    data: {
      channel: `private-user-${userId}`,
      event: 'direct-message',
      data: { text: 'Private message via test' },
    },
  });

  await expect(
    page.getByText('Private message via test')
  ).toBeVisible({ timeout: 10_000 });
});
54% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Presence Channel Member Tracking

Presence channels extend private channels with member awareness. When a user subscribes to a presence- channel, Pusher tracks their membership and broadcasts pusher:member_added and pusher:member_removedevents to all other subscribers. This powers features like “who's online” indicators, typing indicators in chat rooms, and collaborative editing cursors. Testing presence channels requires multiple browser contexts subscribed to the same channel simultaneously.

3

Presence Channel Member Tracking

Complex

Goal

Open two browser contexts authenticated as different users. Subscribe both to the same presence channel. Verify that each user sees the other in the member list. Close one context and verify the member_removed event updates the UI.

Preconditions

  • Two test user accounts exist with different user IDs
  • The auth endpoint returns channel_data with user_id and user_info
  • The page renders a member list from presence channel data

Playwright Implementation

pusher-presence-channel.spec.ts

What to Assert Beyond the UI

  • The subscription_succeeded event includes the correct initial member list
  • The member_added callback fires with the correct user_info payload
  • The member_removed callback fires within Pusher's 30-second timeout after disconnection
  • Duplicate subscriptions from the same user ID do not create duplicate member entries

Assrt Equivalent

# scenarios/pusher-presence-members.assrt
describe: Presence channel member tracking

given:
  - User A is logged in and on /room/123
  - User B is logged in and on /room/123

steps:
  - wait for both users to be subscribed to presence-room-123
  - verify User A sees User B in the member list
  - verify User B sees User A in the member list
  - close User B's browser

expect:
  - User A's member list no longer shows User B
  - User A's member count shows 1

6. Scenario: Client Events Between Peers

Client events are events triggered directly from one browser to another through Pusher, without passing through your server. They must be enabled in your Pusher app settings and can only be sent on private or presence channels. The event name must be prefixed with client-. Common use cases include typing indicators, cursor positions in collaborative editors, and peer-to-peer game state updates. The critical testing challenge is that client events are not delivered back to the sender, so you need two browser contexts to verify delivery.

4

Client Events Between Peers

Complex

Goal

Two authenticated users subscribe to the same private channel. User A triggers a client-typing event. User B sees the typing indicator. Verify that User A (the sender) does not receive their own event.

Playwright Implementation

pusher-client-events.spec.ts

Client Events: Playwright vs Assrt

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

test('client events: typing indicator', async ({ browser }) => {
  const ctxA = await browser.newContext({
    storageState: './test/.auth/user-a.json',
  });
  const ctxB = await browser.newContext({
    storageState: './test/.auth/user-b.json',
  });
  const pageA = await ctxA.newPage();
  const pageB = await ctxB.newPage();

  await pageA.goto('/chat/room-42');
  await pageB.goto('/chat/room-42');

  // Wait for subscription on both
  for (const pg of [pageA, pageB]) {
    await pg.waitForFunction(() => {
      const w = window as any;
      return w.pusher?.channel('private-chat-room-42')?.subscribed;
    }, { timeout: 10_000 });
  }

  await pageA.getByTestId('message-input').type('Hello');

  await expect(
    pageB.getByTestId('typing-indicator')
  ).toBeVisible({ timeout: 5_000 });

  await expect(
    pageA.getByTestId('typing-indicator')
  ).not.toBeVisible();

  await ctxA.close();
  await ctxB.close();
});
57% fewer lines

7. Scenario: Connection State Machine and Reconnection

Pusher's JavaScript client maintains a state machine with seven states. Your application almost certainly binds UI behavior to these states: showing a “reconnecting” banner when connectivity drops, disabling message input during the connecting state, queuing outbound messages during unavailable, and re-enabling everything when connected resumes. If you do not test these transitions, your users will encounter broken UI states the first time their network hiccups.

5

Connection State and Reconnection

Complex

Goal

Establish a Pusher connection, verify the connected state, simulate a network interruption by blocking WebSocket connections, verify the UI shows a reconnecting state, restore connectivity, and verify the client reconnects and resubscribes to all channels automatically.

Playwright Implementation

pusher-connection-state.spec.ts

Assrt Equivalent

# scenarios/pusher-reconnection.assrt
describe: Connection state machine and auto-reconnection

given:
  - I am on /dashboard with an active Pusher connection

steps:
  - verify the connection status shows "connected"
  - simulate a network interruption
  - wait for the connection status to show "reconnecting"
  - restore network connectivity
  - wait for the connection to re-establish

expect:
  - the connection status returns to "connected"
  - the message input is re-enabled
  - all channels are resubscribed
  - a triggered event is delivered after reconnection

8. Scenario: WebSocket Fallback to HTTP Streaming

Pusher's client library supports multiple transports. The preferred transport is WebSocket, but when WebSockets are blocked (corporate proxies, certain mobile networks, restrictive firewall rules), the client falls back to HTTP streaming (via XHR or SockJS). A significant number of enterprise users sit behind proxies that strip or block WebSocket upgrade headers. If you only test with WebSockets, you are missing a transport path that real users depend on.

6

WebSocket Fallback to HTTP

Moderate

Goal

Block all WebSocket connections at the browser level, verify the Pusher client falls back to HTTP streaming, and confirm that events are still delivered correctly over the fallback transport.

Playwright Implementation

pusher-ws-fallback.spec.ts

What to Assert Beyond the UI

  • The transport.name on the connection is not ws
  • Event delivery latency is higher but still within acceptable bounds (under 10 seconds)
  • Channel subscriptions work identically over the fallback transport
  • Private and presence channel auth still functions over HTTP transport

9. Common Pitfalls That Break Pusher Test Suites

Silent Auth Endpoint Failures

The most common Pusher testing failure is a broken auth endpoint that returns a 500 or 403 status. Many Pusher client configurations do not surface auth failures as visible errors. The subscription just never succeeds, and your test hangs until timeout. Always add an explicit check for the auth request and response in your test. Use page.waitForResponse() to intercept the auth response and assert its status code is 200 before waiting for subscription success. GitHub issue pusher-js#574 documents this behavior extensively.

Race Between Subscribe and Trigger

If your test triggers a server-side event before the client has finished subscribing to the channel, the event is lost. Pusher does not queue events for channels that have not yet been subscribed. Always wait for subscription_succeeded before triggering events. Use page.waitForFunction()to poll the channel's subscribed property. This is the single most common cause of flaky Pusher tests in CI, according to Stack Overflow discussions and the Pusher community forum.

Presence Channel Timeout on Member Removed

When a client disconnects from a presence channel, the member_removed event is not instant. Pusher waits for the connection to truly time out before broadcasting the removal, which can take up to 30 seconds depending on the transport. If your test asserts member removal with a 5-second timeout, it will fail intermittently. Set your assertion timeout to at least 30 seconds for member removal checks, or use a deliberate pusher.disconnect() call which triggers faster cleanup than a network drop.

Client Events Disabled in App Settings

Client events are disabled by default in Pusher app settings. If your tests trigger client-prefixed events and nothing happens, check the Pusher dashboard under App Settings and enable “Enable client events.” This setting is per-app, so your production app might have it enabled while your test app does not. The Pusher client silently swallows the error, making it invisible in test output.

CORS Rejecting the Auth Endpoint

The Pusher client sends an XHR POST to your auth endpoint. If your server does not handle CORS for the test origin (for example, http://localhost:3000 in development or a dynamic CI preview URL), the auth request fails at the browser level before it reaches your server. The browser console shows a CORS error, but the Pusher client reports it as a generic auth failure. Always verify CORS headers are configured for all test environments.

Pusher Test Suite Anti-Patterns

  • Triggering events before subscription_succeeded fires
  • Using a 5-second timeout for presence member_removed assertions
  • Not intercepting the auth response to check for 200 status
  • Forgetting to enable client events in the Pusher dashboard
  • Sharing a single Pusher app between production and test
  • Not testing HTTP fallback transport at all
  • Asserting on WebSocket frame content instead of DOM changes
  • Running parallel workers that exceed Pusher connection limits
Common Pusher Auth Failure in Tests
Pusher Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above involves Playwright-specific boilerplate: creating multiple browser contexts, polling window.pusher state with waitForFunction, intercepting auth requests, and managing cleanup. The presence channel test alone is over 50 lines. Multiply that by the eight scenarios you need and you have a test file that is harder to maintain than the feature it tests. Assrt lets you describe each scenario in plain English and generates the Playwright implementation automatically.

The presence channel scenario from Section 5 demonstrates the value clearly. In raw Playwright, you manage two browser contexts, poll subscription state on both, coordinate timing between join and leave, and handle cleanup. In Assrt, you state the intent and let the framework handle the mechanics.

scenarios/pusher-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections. The generated code is committed to your repo as real test files you can read, run, and modify. When Pusher updates their client library, changes their auth response format, or modifies WebSocket handshake behavior, Assrt detects the failure, analyzes the new behavior, and opens a pull request with updated test code. Your scenario files stay untouched.

Start with the public channel smoke test. Once it passes in CI, add private channel auth, then presence member tracking, then client events, then connection state testing, and finally WebSocket fallback. In a single afternoon you can have comprehensive Pusher realtime coverage that most applications never achieve manually.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk