Skip to main content Skills Marketplace Descubre y explora habilidades de IA creadas por la comunidad.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Copiar promptMostrar detalles del prompt Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/PramodDutta/qaskills --skill websocket-bug-finderEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Descargar Zip Descargando... Más de este repositorio Use when publishing SEO blog articles to qaskills.sh, e.g. "publish today's articles", "daily SEO batch", "write 10 articles from keyword research", "add a blog post", or any request that creates files under packages/web/src/app/blog/posts.
secure-test-data-engineer Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.
Ocupaciones relacionadas SOC
Basado en la clasificación ocupacional SOC
name WebSocket Bug Finder description Test WebSocket connections for reliability including reconnection logic, message ordering, heartbeat mechanisms, and connection state management under adverse conditions version 1.0.0 author Pramod license MIT tags ["websocket","real-time","connection-testing","reconnection","message-ordering","heartbeat","socket-testing","ws"] testingTypes ["integration","e2e"] frameworks ["playwright"] languages ["typescript","javascript"] domains ["web","api"] agents ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp"]
WebSocket Bug Finder Skill
You are an expert QA automation engineer specializing in WebSocket and real-time communication testing. When the user asks you to write, review, or debug WebSocket tests, follow these detailed instructions to validate connection lifecycle management, reconnection reliability, message ordering guarantees, heartbeat mechanisms, and connection behavior under adverse network conditions.
Core Principles
Connections are ephemeral -- WebSocket connections will drop unexpectedly due to network changes, server restarts, load balancer timeouts, and mobile device sleep. Every WebSocket client must handle disconnection gracefully and reconnect automatically.
Message ordering is not guaranteed without explicit sequencing -- While TCP guarantees in-order delivery within a single connection, reconnection creates a new TCP stream. Messages sent during reconnection may arrive out of order. Test that applications handle sequence gaps correctly.
Heartbeats are a contract -- Both client and server must participate in keep-alive mechanisms. Test that heartbeats are sent at the correct interval, that missed heartbeats trigger reconnection, and that heartbeat failures do not cause silent connection death.
Test the transitions, not just the states -- The critical bugs live in state transitions: connecting to open, open to closing, closing to closed, closed to reconnecting. Test every transition path, especially the error paths.
Simulate real network conditions -- Lab environments with perfect connectivity will never expose reconnection bugs. Use network throttling, packet loss simulation, and connection interruption to test under realistic conditions.
Binary and text frames have different semantics -- WebSocket supports both text frames (UTF-8 encoded) and binary frames (ArrayBuffer). Test that the application correctly handles both frame types and does not confuse them.
Concurrency limits matter -- Browsers limit WebSocket connections per domain (typically 6-30). Test that the application functions correctly near these limits and handles connection pool exhaustion gracefully.
Project Structure
Organize WebSocket testing projects with this structure:
tests/
websocket/
connection/
lifecycle.spec.ts
reconnection.spec.ts
authentication.spec.ts
concurrent-connections.spec.ts
messaging/
ordering.spec.ts
delivery-guarantee.spec.ts
binary-messages.spec.ts
large-payloads.spec.ts
heartbeat/
ping-pong.spec.ts
timeout-detection.spec.ts
keep-alive.spec.ts
resilience/
network-disruption.spec.ts
server-restart.spec.ts
backpressure.spec.ts
e2e/
real-time-updates.spec.ts
collaborative-editing.spec.ts
chat-messaging.spec.ts
helpers/
ws-test-client.ts
ws-message-recorder.ts
network-simulator.ts
ws-server-mock.ts
fixtures/
websocket.fixture.ts
config/
ws-test-config.ts
playwright.config.ts
WebSocket Connection Lifecycle Testing
Test Client Helper
import WebSocket from 'ws' ;
import { EventEmitter } from 'events' ;
interface WSMessage {
data : string | Buffer ;
timestamp : number ;
type : 'text' | 'binary' ;
sequence ?: number ;
}
interface ConnectionEvent {
type : 'open' | 'close' | 'error' | 'message' | 'reconnect' ;
timestamp : number ;
details ?: unknown ;
}
export class WSTestClient extends EventEmitter {
private ws : WebSocket | null = null ;
private url : string ;
private messages : WSMessage [] = [];
private events : ConnectionEvent [] = [];
private reconnectAttempts = 0 ;
private maxReconnectAttempts = 5 ;
private reconnectDelay = 1000 ;
private shouldReconnect = true ;
private heartbeatInterval : NodeJS .Timeout | null = null ;
private heartbeatTimeout : NodeJS .Timeout | null = null ;
private headers : Record <string , string >;
constructor (
url : string ,
options : {
headers?: Record<string , string >;
maxReconnectAttempts?: number ;
reconnectDelay?: number ;
} = {}
) {
super ();
this .url = url;
this .headers = options.headers || {};
this .maxReconnectAttempts = options.maxReconnectAttempts ?? 5 ;
this .reconnectDelay = options.reconnectDelay ?? 1000 ;
}
connect (): Promise <void > {
return new Promise ((resolve, reject ) => {
this .ws = new WebSocket (this .url , { headers : this .headers });
this .ws .on ('open' , () => {
this .reconnectAttempts = 0 ;
this .recordEvent ('open' );
this .emit ('open' );
resolve ();
});
this .ws .on ('message' , (data : WebSocket .Data ) => {
const message : WSMessage = {
data : data instanceof Buffer ? data : data.toString (),
timestamp : Date .now (),
type : data instanceof Buffer ? 'binary' : 'text' ,
};
if (typeof message.data === 'string' ) {
try {
const parsed = JSON .parse (message.data );
if (parsed.seq !== undefined ) {
message.sequence = parsed.seq ;
}
} catch {
}
}
this .messages .push (message);
this .recordEvent ('message' , message);
this .emit ('message' , message);
});
this .ws .on ('close' , (code, reason ) => {
this .recordEvent ('close' , { code, reason : reason.toString () });
this .emit ('close' , code, reason);
this .stopHeartbeat ();
if (this .shouldReconnect && this .reconnectAttempts < this .maxReconnectAttempts ) {
this .attemptReconnect ();
}
});
this .ws .on ('error' , (error ) => {
this .recordEvent ('error' , { message : error.message });
this .emit ('error' , error);
reject (error);
});
});
}
private async attemptReconnect (): Promise <void > {
this .reconnectAttempts ++;
const delay = this .reconnectDelay * Math .pow (2 , this .reconnectAttempts - 1 );
this .recordEvent ('reconnect' , { attempt : this .reconnectAttempts , delay });
await new Promise ((resolve ) => setTimeout (resolve, delay));
try {
await this .connect ();
} catch {
}
}
send (data : string | Buffer ): void {
if (this .ws ?.readyState !== WebSocket .OPEN ) {
throw new Error (`Cannot send: WebSocket is ${this .getStateName()} ` );
}
this .ws .send (data);
}
startHeartbeat (intervalMs : number = 30000 , timeoutMs : number = 5000 ): void {
this .heartbeatInterval = setInterval (() => {
if (this .ws ?.readyState === WebSocket .OPEN ) {
this .ws .ping ();
this .heartbeatTimeout = setTimeout (() => {
this .emit ('heartbeat-timeout' );
this .ws ?.terminate ();
}, timeoutMs);
}
}, intervalMs);
this .ws ?.on ('pong' , () => {
if (this .heartbeatTimeout ) {
clearTimeout (this .heartbeatTimeout );
this .heartbeatTimeout = null ;
}
this .emit ('pong' );
});
}
stopHeartbeat (): void {
if (this .heartbeatInterval ) {
clearInterval (this .heartbeatInterval );
this .heartbeatInterval = null ;
}
if (this .heartbeatTimeout ) {
clearTimeout (this .heartbeatTimeout );
this .heartbeatTimeout = null ;
}
}
close (code : number = 1000 , reason : string = 'Test complete' ): void {
this .shouldReconnect = false ;
this .stopHeartbeat ();
this .ws ?.close (code, reason);
}
getMessages (): WSMessage [] {
return [...this .messages ];
}
getEvents (): ConnectionEvent [] {
return [...this .events ];
}
getState (): number {
return this .ws ?.readyState ?? WebSocket .CLOSED ;
}
getStateName (): string {
const states : Record <number , string > = {
[WebSocket .CONNECTING ]: 'CONNECTING' ,
[WebSocket .OPEN ]: 'OPEN' ,
[WebSocket .CLOSING ]: 'CLOSING' ,
[WebSocket .CLOSED ]: 'CLOSED' ,
};
return states[this .getState ()] || 'UNKNOWN' ;
}
getReconnectAttempts (): number {
return this .reconnectAttempts ;
}
clearMessages (): void {
this .messages = [];
}
private recordEvent (type : ConnectionEvent ['type' ], details ?: unknown ): void {
this .events .push ({ type , timestamp : Date .now (), details });
}
}
Connection Lifecycle Tests
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer , WebSocket } from 'ws' ;
let wss : WebSocketServer ;
const PORT = 8765 ;
const WS_URL = `ws://localhost:${PORT} ` ;
beforeAll (() => {
wss = new WebSocketServer ({ port : PORT });
wss.on ('connection' , (ws ) => {
ws.on ('message' , (data ) => {
ws.send (data);
});
});
});
afterAll (() => {
wss.close ();
});
describe ('WebSocket Connection Lifecycle' , () => {
let client : WSTestClient ;
afterEach (() => {
client?.close ();
});
test ('should establish connection and reach OPEN state' , async () => {
client = new WSTestClient (WS_URL );
await client.connect ();
expect (client.getState ()).toBe (WebSocket .OPEN );
expect (client.getStateName ()).toBe ('OPEN' );
const events = client.getEvents ();
expect (events[0 ].type ).toBe ('open' );
});
test ('should perform clean close with code 1000' , async () => {
client = new WSTestClient (WS_URL );
await client.connect ();
const closePromise = new Promise <{ code : number ; reason : Buffer }>((resolve ) => {
client.on ('close' , (code, reason ) => resolve ({ code, reason }));
});
client.close (1000 , 'Normal closure' );
const result = await closePromise;
expect (result.code ).toBe (1000 );
expect (client.getState ()).toBe (WebSocket .CLOSED );
});
test ('should record all lifecycle events in order' , async () => {
client = new WSTestClient (WS_URL );
await client.connect ();
client.send ('test message' );
await new Promise <void >((resolve ) => {
client.on ('message' , () => resolve ());
});
client.close ();
await new Promise <void >((resolve ) => {
client.on ('close' , () => resolve ());
});
const events = client.getEvents ();
const eventTypes = events.map ((e ) => e.type );
expect (eventTypes[0 ]).toBe ('open' );
expect (eventTypes).toContain ('message' );
expect (eventTypes[eventTypes.length - 1 ]).toBe ('close' );
for (let i = 1 ; i < events.length ; i++) {
expect (events[i].timestamp ).toBeGreaterThanOrEqual (events[i - 1 ].timestamp );
}
});
test ('should handle server-initiated close' , async () => {
client = new WSTestClient (WS_URL , { maxReconnectAttempts : 0 });
await client.connect ();
const closePromise = new Promise <number >((resolve ) => {
client.on ('close' , (code ) => resolve (code));
});
wss.clients .forEach ((ws ) => {
ws.close (1001 , 'Server going away' );
});
const code = await closePromise;
expect (code).toBe (1001 );
});
test ('should reject connection to invalid URL' , async () => {
client = new WSTestClient ('ws://localhost:9999' , { maxReconnectAttempts : 0 });
await expect (client.connect ()).rejects .toThrow ();
expect (client.getState ()).toBe (WebSocket .CLOSED );
});
});
Reconnection Logic Verification
import { describe, test, expect, afterEach } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer , WebSocket } from 'ws' ;
describe ('WebSocket Reconnection' , () => {
let wss : WebSocketServer ;
let client : WSTestClient ;
const PORT = 8766 ;
afterEach (() => {
client?.close ();
wss?.close ();
});
test ('should automatically reconnect after server disconnect' , async () => {
wss = new WebSocketServer ({ port : PORT });
client = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : 3 ,
reconnectDelay : 100 ,
});
await client.connect ();
expect (client.getState ()).toBe (WebSocket .OPEN );
const reconnectPromise = new Promise <void >((resolve ) => {
client.on ('open' , () => resolve ());
});
wss.clients .forEach ((ws ) => ws.terminate ());
await reconnectPromise;
expect (client.getState ()).toBe (WebSocket .OPEN );
expect (client.getReconnectAttempts ()).toBeGreaterThanOrEqual (1 );
});
test ('should use exponential backoff for reconnection attempts' , async () => {
wss = new WebSocketServer ({ port : PORT });
client = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : 4 ,
reconnectDelay : 100 ,
});
await client.connect ();
wss.close ();
client['ws' ]?.terminate ();
await new Promise ((resolve ) => setTimeout (resolve, 5000 ));
const events = client.getEvents ().filter ((e ) => e.type === 'reconnect' );
for (let i = 1 ; i < events.length ; i++) {
const prevDelay = (events[i - 1 ].details as { delay : number }).delay ;
const currDelay = (events[i].details as { delay : number }).delay ;
expect (currDelay).toBeGreaterThanOrEqual (prevDelay);
}
});
test ('should stop reconnecting after max attempts' , async () => {
const maxAttempts = 3 ;
wss = new WebSocketServer ({ port : PORT });
client = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : maxAttempts,
reconnectDelay : 50 ,
});
await client.connect ();
wss.close ();
client['ws' ]?.terminate ();
await new Promise ((resolve ) => setTimeout (resolve, 3000 ));
const reconnectEvents = client.getEvents ().filter ((e ) => e.type === 'reconnect' );
expect (reconnectEvents.length ).toBeLessThanOrEqual (maxAttempts);
});
test ('should reset reconnect counter after successful connection' , async () => {
wss = new WebSocketServer ({ port : PORT });
client = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : 5 ,
reconnectDelay : 100 ,
});
await client.connect ();
const firstReconnect = new Promise <void >((resolve ) => {
client.once ('open' , () => resolve ());
});
wss.clients .forEach ((ws ) => ws.terminate ());
await firstReconnect;
expect (client.getReconnectAttempts ()).toBe (0 );
const secondReconnect = new Promise <void >((resolve ) => {
client.once ('open' , () => resolve ());
});
wss.clients .forEach ((ws ) => ws.terminate ());
await secondReconnect;
expect (client.getState ()).toBe (WebSocket .OPEN );
});
});
Message Ordering and Delivery Guarantees
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer } from 'ws' ;
describe ('Message Ordering' , () => {
let wss : WebSocketServer ;
let client : WSTestClient ;
const PORT = 8767 ;
beforeAll (() => {
wss = new WebSocketServer ({ port : PORT });
wss.on ('connection' , (ws ) => {
ws.on ('message' , (data ) => {
const msg = JSON .parse (data.toString ());
ws.send (
JSON .stringify ({
...msg,
serverTimestamp : Date .now (),
})
);
});
});
});
afterAll (() => wss.close ());
afterEach (() => client?.close ());
test ('messages should arrive in order within a single connection' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
const messageCount = 100 ;
const receivedPromise = new Promise <void >((resolve ) => {
let count = 0 ;
client.on ('message' , () => {
count++;
if (count === messageCount) resolve ();
});
});
for (let i = 0 ; i < messageCount; i++) {
client.send (JSON .stringify ({ seq : i, data : `message-${i} ` }));
}
await receivedPromise;
const messages = client.getMessages ();
expect (messages).toHaveLength (messageCount);
for (let i = 0 ; i < messages.length ; i++) {
const parsed = JSON .parse (messages[i].data as string );
expect (parsed.seq ).toBe (i);
}
});
test ('should detect out-of-order messages' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
const reorderWss = new WebSocketServer ({ port : PORT + 1 });
const reorderClient = new WSTestClient (`ws://localhost:${PORT + 1 } ` );
reorderWss.on ('connection' , (ws ) => {
const buffer : string [] = [];
ws.on ('message' , (data ) => {
buffer.push (data.toString ());
if (buffer.length === 3 ) {
buffer.reverse ().forEach ((msg ) => ws.send (msg));
buffer.length = 0 ;
}
});
});
await reorderClient.connect ();
const receivedPromise = new Promise <void >((resolve ) => {
let count = 0 ;
reorderClient.on ('message' , () => {
count++;
if (count === 9 ) resolve ();
});
});
for (let i = 0 ; i < 9 ; i++) {
reorderClient.send (JSON .stringify ({ seq : i }));
}
await receivedPromise;
const messages = reorderClient.getMessages ();
const sequences = messages.map ((m ) => JSON .parse (m.data as string ).seq );
let outOfOrderCount = 0 ;
for (let i = 1 ; i < sequences.length ; i++) {
if (sequences[i] < sequences[i - 1 ]) {
outOfOrderCount++;
}
}
expect (outOfOrderCount).toBeGreaterThan (0 );
reorderClient.close ();
reorderWss.close ();
});
test ('should handle duplicate message detection' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
const dupWss = new WebSocketServer ({ port : PORT + 2 });
const dupClient = new WSTestClient (`ws://localhost:${PORT + 2 } ` );
dupWss.on ('connection' , (ws ) => {
ws.on ('message' , (data ) => {
ws.send (data);
ws.send (data);
});
});
await dupClient.connect ();
const receivedPromise = new Promise <void >((resolve ) => {
let count = 0 ;
dupClient.on ('message' , () => {
count++;
if (count === 6 ) resolve ();
});
});
for (let i = 0 ; i < 3 ; i++) {
dupClient.send (JSON .stringify ({ id : `msg-${i} ` , seq : i }));
}
await receivedPromise;
const messages = dupClient.getMessages ();
const uniqueIds = new Set (messages.map ((m ) => JSON .parse (m.data as string ).id ));
expect (messages.length ).toBe (6 );
expect (uniqueIds.size ).toBe (3 );
dupClient.close ();
dupWss.close ();
});
});
Heartbeat and Ping-Pong Testing
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer } from 'ws' ;
describe ('Heartbeat / Ping-Pong' , () => {
let wss : WebSocketServer ;
let client : WSTestClient ;
const PORT = 8768 ;
beforeAll (() => {
wss = new WebSocketServer ({ port : PORT });
wss.on ('connection' , (ws ) => {
ws.on ('message' , (data ) => ws.send (data));
});
});
afterAll (() => wss.close ());
afterEach (() => client?.close ());
test ('client should send periodic heartbeat pings' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
let pongCount = 0 ;
client.on ('pong' , () => pongCount++);
client.startHeartbeat (200 , 100 );
await new Promise ((resolve ) => setTimeout (resolve, 1000 ));
expect (pongCount).toBeGreaterThanOrEqual (3 );
});
test ('should detect heartbeat timeout when server stops responding' , async () => {
const silentWss = new WebSocketServer ({ port : PORT + 1 });
silentWss.on ('connection' , (ws ) => {
ws.on ('ping' , () => {
});
});
client = new WSTestClient (`ws://localhost:${PORT + 1 } ` , {
maxReconnectAttempts : 0 ,
});
await client.connect ();
const timeoutPromise = new Promise <void >((resolve ) => {
client.on ('heartbeat-timeout' , () => resolve ());
});
client.startHeartbeat (200 , 100 );
await timeoutPromise;
await new Promise ((resolve ) => setTimeout (resolve, 200 ));
expect (client.getState ()).not .toBe (1 );
silentWss.close ();
});
test ('heartbeat should reset timeout on each successful pong' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
let timeoutOccurred = false ;
client.on ('heartbeat-timeout' , () => {
timeoutOccurred = true ;
});
client.startHeartbeat (100 , 500 );
await new Promise ((resolve ) => setTimeout (resolve, 2000 ));
expect (timeoutOccurred).toBe (false );
expect (client.getState ()).toBe (1 );
});
});
Connection Drop Simulation
Simulating Network Disruptions in E2E Tests
import { test, expect, Page } from '@playwright/test' ;
test.describe ('WebSocket Network Disruption' , () => {
test ('should reconnect after network interruption' , async ({ page, context }) => {
await page.goto ('/chat' );
await page.waitForLoadState ('networkidle' );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText ('Connected' );
await page.fill ('[data-testid="message-input"]' , 'Hello before disconnect' );
await page.click ('[data-testid="send-button"]' );
await expect (page.locator ('[data-testid="messages"] >> text=Hello before disconnect' )).toBeVisible ();
await context.setOffline (true );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText (
/disconnected|reconnecting/i ,
{ timeout : 10000 }
);
await context.setOffline (false );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText (
'Connected' ,
{ timeout : 15000 }
);
await page.fill ('[data-testid="message-input"]' , 'Hello after reconnect' );
await page.click ('[data-testid="send-button"]' );
await expect (
page.locator ('[data-testid="messages"] >> text=Hello after reconnect' )
).toBeVisible ();
});
test ('should queue messages sent during disconnection' , async ({ page, context }) => {
await page.goto ('/chat' );
await page.waitForLoadState ('networkidle' );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText ('Connected' );
await context.setOffline (true );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText (
/disconnected|reconnecting/i ,
{ timeout : 10000 }
);
await page.fill ('[data-testid="message-input"]' , 'Queued message 1' );
await page.click ('[data-testid="send-button"]' );
await page.fill ('[data-testid="message-input"]' , 'Queued message 2' );
await page.click ('[data-testid="send-button"]' );
await expect (page.locator ('[data-testid="pending-indicator"]' ).first ()).toBeVisible ();
await context.setOffline (false );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText (
'Connected' ,
{ timeout : 15000 }
);
await expect (page.locator ('[data-testid="pending-indicator"]' )).toHaveCount (0 , {
timeout : 10000 ,
});
await expect (page.locator ('text=Queued message 1' )).toBeVisible ();
await expect (page.locator ('text=Queued message 2' )).toBeVisible ();
});
test ('should handle slow network gracefully' , async ({ page }) => {
const cdpSession = await page.context ().newCDPSession (page);
await cdpSession.send ('Network.emulateNetworkConditions' , {
offline : false ,
downloadThroughput : 5000 ,
uploadThroughput : 5000 ,
latency : 2000 ,
});
await page.goto ('/chat' );
await expect (page.locator ('[data-testid="connection-status"]' )).toHaveText (
'Connected' ,
{ timeout : 30000 }
);
await page.fill ('[data-testid="message-input"]' , 'Slow network message' );
await page.click ('[data-testid="send-button"]' );
await expect (page.locator ('text=Slow network message' )).toBeVisible ({
timeout : 15000 ,
});
await cdpSession.send ('Network.emulateNetworkConditions' , {
offline : false ,
downloadThroughput : -1 ,
uploadThroughput : -1 ,
latency : 0 ,
});
});
});
Authentication Token Refresh During WebSocket Sessions
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer , WebSocket } from 'ws' ;
import { IncomingMessage } from 'http' ;
describe ('WebSocket Authentication' , () => {
let wss : WebSocketServer ;
let client : WSTestClient ;
const PORT = 8769 ;
beforeAll (() => {
wss = new WebSocketServer ({
port : PORT ,
verifyClient : (info : { req: IncomingMessage } ) => {
const token = info.req .headers ['authorization' ];
return token === 'Bearer valid-token' || token === 'Bearer refreshed-token' ;
},
});
wss.on ('connection' , (ws ) => {
ws.send (JSON .stringify ({ type : 'auth_success' }));
ws.on ('message' , (data ) => ws.send (data));
});
});
afterAll (() => wss.close ());
afterEach (() => client?.close ());
test ('should connect with valid authentication token' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` , {
headers : { Authorization : 'Bearer valid-token' },
});
await client.connect ();
expect (client.getState ()).toBe (WebSocket .OPEN );
await new Promise <void >((resolve ) => {
client.on ('message' , (msg ) => {
const parsed = JSON .parse (msg.data as string );
if (parsed.type === 'auth_success' ) resolve ();
});
});
});
test ('should reject connection with invalid token' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` , {
headers : { Authorization : 'Bearer invalid-token' },
maxReconnectAttempts : 0 ,
});
await expect (client.connect ()).rejects .toThrow ();
});
test ('should reconnect with refreshed token after auth expiry' , async () => {
let connectionCount = 0 ;
const authWss = new WebSocketServer ({
port : PORT + 1 ,
verifyClient : (info : { req: IncomingMessage } ) => {
connectionCount++;
if (connectionCount === 1 ) return true ;
if (connectionCount === 2 ) return false ;
return true ;
},
});
authWss.on ('connection' , (ws ) => {
ws.on ('message' , (data ) => ws.send (data));
});
client = new WSTestClient (`ws://localhost:${PORT + 1 } ` , {
maxReconnectAttempts : 3 ,
reconnectDelay : 100 ,
});
await client.connect ();
expect (client.getState ()).toBe (WebSocket .OPEN );
authWss.clients .forEach ((ws ) => ws.close (4001 , 'Token expired' ));
await new Promise ((resolve ) => setTimeout (resolve, 2000 ));
expect (connectionCount).toBeGreaterThanOrEqual (2 );
authWss.close ();
});
});
Backpressure Testing
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer , WebSocket } from 'ws' ;
describe ('WebSocket Backpressure' , () => {
let wss : WebSocketServer ;
let client : WSTestClient ;
const PORT = 8770 ;
beforeAll (() => {
wss = new WebSocketServer ({ port : PORT });
wss.on ('connection' , (ws ) => {
ws.on ('message' , (data ) => {
setTimeout (() => ws.send (data), 10 );
});
});
});
afterAll (() => wss.close ());
afterEach (() => client?.close ());
test ('should handle burst of messages without dropping' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
const messageCount = 500 ;
let received = 0 ;
const allReceived = new Promise <void >((resolve ) => {
client.on ('message' , () => {
received++;
if (received === messageCount) resolve ();
});
});
for (let i = 0 ; i < messageCount; i++) {
client.send (JSON .stringify ({ seq : i, data : 'x' .repeat (100 ) }));
}
await allReceived;
expect (received).toBe (messageCount);
const messages = client.getMessages ();
const sequences = messages.map ((m ) => JSON .parse (m.data as string ).seq );
const uniqueSequences = new Set (sequences);
expect (uniqueSequences.size ).toBe (messageCount);
});
test ('should handle large payloads' , async () => {
client = new WSTestClient (`ws://localhost:${PORT} ` );
await client.connect ();
const sizes = [1024 , 10240 , 102400 , 1048576 ];
for (const size of sizes) {
const payload = JSON .stringify ({
size,
data : 'x' .repeat (size),
});
const responsePromise = new Promise <void >((resolve ) => {
client.once ('message' , () => resolve ());
});
client.send (payload);
await responsePromise;
const lastMessage = client.getMessages ().at (-1 );
expect (lastMessage).toBeDefined ();
const parsed = JSON .parse (lastMessage!.data as string );
expect (parsed.data .length ).toBe (size);
}
});
});
Concurrent Connection Limits
import { describe, test, expect, beforeAll, afterAll } from 'vitest' ;
import { WSTestClient } from '../../helpers/ws-test-client' ;
import { WebSocketServer } from 'ws' ;
describe ('Concurrent WebSocket Connections' , () => {
let wss : WebSocketServer ;
const PORT = 8771 ;
const MAX_CONNECTIONS = 10 ;
beforeAll (() => {
wss = new WebSocketServer ({ port : PORT });
let connectionCount = 0 ;
wss.on ('connection' , (ws ) => {
connectionCount++;
if (connectionCount > MAX_CONNECTIONS ) {
ws.close (4029 , 'Too many connections' );
return ;
}
ws.on ('message' , (data ) => ws.send (data));
ws.on ('close' , () => connectionCount--);
});
});
afterAll (() => wss.close ());
test ('should handle multiple concurrent connections' , async () => {
const clients : WSTestClient [] = [];
const count = 5 ;
for (let i = 0 ; i < count; i++) {
const client = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : 0 ,
});
await client.connect ();
clients.push (client);
}
for (const client of clients) {
expect (client.getStateName ()).toBe ('OPEN' );
}
for (let i = 0 ; i < clients.length ; i++) {
const responsePromise = new Promise <void >((resolve ) => {
clients[i].once ('message' , () => resolve ());
});
clients[i].send (JSON .stringify ({ client : i }));
await responsePromise;
}
clients.forEach ((c ) => c.close ());
});
test ('should reject connections beyond the limit' , async () => {
const clients : WSTestClient [] = [];
for (let i = 0 ; i < MAX_CONNECTIONS ; i++) {
const client = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : 0 ,
});
await client.connect ();
clients.push (client);
}
const extraClient = new WSTestClient (`ws://localhost:${PORT} ` , {
maxReconnectAttempts : 0 ,
});
await extraClient.connect ();
const closePromise = new Promise <number >((resolve ) => {
extraClient.on ('close' , (code ) => resolve (code));
});
const code = await closePromise;
expect (code).toBe (4029 );
clients.forEach ((c ) => c.close ());
extraClient.close ();
});
});
Configuration
Playwright Configuration for WebSocket E2E Tests
import { defineConfig, devices } from '@playwright/test' ;
export default defineConfig ({
testDir : './tests/websocket/e2e' ,
fullyParallel : false ,
retries : 2 ,
timeout : 30000 ,
reporter : [
['html' , { open : 'never' }],
['json' , { outputFile : 'ws-test-results.json' }],
],
use : {
baseURL : process.env .BASE_URL || 'http://localhost:3000' ,
trace : 'on-first-retry' ,
screenshot : 'only-on-failure' ,
video : 'retain-on-failure' ,
},
projects : [
{
name : 'ws-chrome' ,
use : { ...devices['Desktop Chrome' ] },
},
{
name : 'ws-firefox' ,
use : { ...devices['Desktop Firefox' ] },
},
{
name : 'ws-mobile' ,
use : { ...devices['iPhone 14' ] },
},
],
});
WebSocket Test Configuration
export const WS_TEST_CONFIG = {
serverUrl : process.env .WS_SERVER_URL || 'ws://localhost:8080' ,
serverPort : parseInt (process.env .WS_TEST_PORT || '8080' , 10 ),
connectTimeout : 5000 ,
maxReconnectAttempts : 5 ,
reconnectBaseDelay : 1000 ,
reconnectMaxDelay : 30000 ,
heartbeatInterval : 30000 ,
heartbeatTimeout : 5000 ,
maxMessageSize : 1048576 ,
messageTimeout : 10000 ,
burstMessageCount : 500 ,
concurrentClientCount : 10 ,
largePayloadSizes : [1024 , 10240 , 102400 , 1048576 ],
};
Best Practices
Always implement exponential backoff with jitter for reconnection -- A fixed reconnect interval causes all disconnected clients to reconnect simultaneously, creating a "thundering herd" that can overwhelm the server. Add random jitter to spread reconnection attempts.
Use connection state machines -- Model the WebSocket lifecycle as a state machine with explicit states (DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING) and valid transitions. This prevents impossible state combinations like sending messages while disconnected.
Implement message acknowledgment for critical operations -- For messages that must not be lost (e.g., financial transactions, chat messages), implement application-level acknowledgments. Do not rely solely on TCP delivery guarantees.
Test with real-world disconnect scenarios -- Lab-perfect connections hide bugs. Test with WiFi-to-cellular transitions, VPN disconnects, laptop lid close/open, and browser tab backgrounding.
Add sequence numbers to messages -- Every message should include a monotonically increasing sequence number. This enables detection of gaps, duplicates, and reordering on the receiving end.
Handle the "half-open" connection state -- A TCP connection can appear open on one side while closed on the other. Heartbeats detect this condition. Without heartbeats, a client may believe it is connected while the server has already dropped the connection.
Buffer messages during reconnection -- Messages sent while the WebSocket is reconnecting should be queued and delivered once the connection is re-established, not silently dropped.
Test binary message handling separately from text -- Binary frames (ArrayBuffer, Blob) and text frames have different serialization paths. Test both frame types to ensure the application handles each correctly.
Implement connection pooling for multiple channels -- Applications that need multiple logical channels should multiplex over a single WebSocket connection rather than opening separate connections per channel.
Set appropriate close codes -- Use RFC 6455 close codes correctly: 1000 (normal), 1001 (going away), 1008 (policy violation), 1011 (unexpected condition). Custom codes should be in the 4000-4999 range.
Test WebSocket behavior across browser tabs -- Browsers may throttle or suspend WebSocket connections in background tabs. Test that reconnection works correctly when a user returns to a backgrounded tab.
Monitor WebSocket connection metrics in production -- Track connection duration, reconnection frequency, message latency, and error rates. These metrics reveal reliability issues that tests alone cannot catch.
Anti-Patterns to Avoid
Reconnecting immediately without backoff -- Instant reconnection creates a retry storm that wastes bandwidth and can trigger rate limiting or server overload. Always use exponential backoff.
Silently dropping messages during disconnection -- When a user sends a chat message during a brief disconnect and the message disappears, it erodes trust. Queue messages and deliver them after reconnection.
Using WebSocket as the only data channel -- WebSocket connections are not guaranteed to stay open. Critical operations should have an HTTP fallback. Do not build flows that are impossible to complete without a persistent WebSocket.
Ignoring close frames and codes -- Different close codes have different meanings. Code 1001 (going away) suggests the server is restarting and reconnection will likely succeed. Code 1008 (policy violation) suggests the client should not reconnect.
Opening a new WebSocket per request -- WebSocket's advantage is persistent connections. Opening and closing a WebSocket for each message negates the protocol's benefits and adds significant overhead.
Trusting message order across reconnections -- A new WebSocket connection is a new TCP stream. Messages in transit during reconnection may be lost or arrive on the new connection out of order. Always use sequence numbers.
Not testing concurrent connection limits -- Browsers enforce per-domain WebSocket limits. Applications that open too many connections will silently fail to connect, causing features to break with no visible error.
Debugging Tips
Use Chrome DevTools WebSocket inspector -- The Network tab in Chrome DevTools shows individual WebSocket frames (both sent and received). Filter by "WS" to see only WebSocket traffic. Click on a connection to inspect individual frames.
Log connection state transitions -- Add logging for every state change: CONNECTING, OPEN, CLOSING, CLOSED, and any custom states like RECONNECTING. This trace is invaluable for debugging intermittent connection issues.
Check for load balancer idle timeout -- Many load balancers (AWS ALB, nginx) close idle WebSocket connections after 60 seconds. If connections drop without heartbeat activity, the load balancer timeout is the likely culprit.
Verify WebSocket upgrade headers -- Use curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" to verify the server responds with a 101 Switching Protocols response. A 200 OK indicates the upgrade failed.
Test with wscat for quick manual verification -- The wscat command-line tool (npx wscat -c ws://localhost:8080) provides a simple way to interactively test WebSocket endpoints without building a test client.
Monitor readyState before sending -- Always check ws.readyState === WebSocket.OPEN before calling ws.send(). Sending on a non-OPEN socket throws an error that may not be caught by error boundaries.
Check for CORS issues on WebSocket upgrade -- While the WebSocket protocol itself does not enforce CORS, some reverse proxies and CDNs may block WebSocket upgrade requests based on Origin headers. Check server logs for rejected upgrade requests.
Use Wireshark to inspect WebSocket frames at the protocol level -- When high-level debugging is insufficient, Wireshark can decode WebSocket frames and show the raw binary content, opcode, masking, and frame boundaries.
Verify that ping/pong frames are not confused with application messages -- WebSocket control frames (ping, pong, close) are distinct from data frames. Ensure your message handler does not process control frames as application messages.
Test reconnection with server-side logging -- Log connection and disconnection events on the server with client identifiers. Compare server-side logs with client-side reconnection logs to identify mismatches where the client believes it is connected but the server does not have a matching connection.