Files
vikunja/frontend/tests/support/websocket.ts
kolaente 4cd79088d1 test: add WebSocket e2e tests
Add comprehensive end-to-end tests for the WebSocket system:

- Protocol tests: auth (valid/invalid token, timeout, double auth),
  subscriptions (valid/invalid event, auth required, unsubscribe),
  message delivery (notification on team add, doer exclusion,
  multi-connection)
- Frontend integration tests: notification badge update, dropdown
  rendering, and logout cleanup via browser-level Playwright tests
- Comment notification test: full flow where user B mentions user A
  in a task comment and user A receives real-time WebSocket notification

Includes ws test dependency, shared test helper utilities, and
cascade-truncation of notifications when truncating users to prevent
test pollution.
2026-04-02 16:30:23 +00:00

95 lines
2.3 KiB
TypeScript

import WebSocket from 'ws'
const API_URL = process.env.API_URL || 'http://localhost:3456/api/v1'
export interface WsMessage {
event?: string
action?: string
success?: boolean
error?: string
data?: unknown
}
/**
* Returns the WebSocket URL derived from the API base URL.
*/
export function getWsUrl(): string {
return API_URL.replace(/\/+$/, '').replace(/^http/, 'ws') + '/ws'
}
/**
* Opens a raw WebSocket connection to the API.
*/
export function openWs(): Promise<WebSocket> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(getWsUrl())
ws.on('open', () => resolve(ws))
ws.on('error', reject)
})
}
/**
* Waits for the next message on a WebSocket connection.
*/
export function waitForMessage(ws: WebSocket, timeout = 5000): Promise<WsMessage> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('WebSocket message timeout')), timeout)
ws.once('message', (data) => {
clearTimeout(timer)
resolve(JSON.parse(data.toString()))
})
})
}
/**
* Sends a JSON message on the WebSocket.
*/
export function sendMessage(ws: WebSocket, msg: object): void {
ws.send(JSON.stringify(msg))
}
/**
* Authenticates a WebSocket connection and returns the auth.success message.
*/
export async function authenticateWs(ws: WebSocket, token: string): Promise<WsMessage> {
sendMessage(ws, {action: 'auth', token})
const msg = await waitForMessage(ws)
if (msg.action !== 'auth.success') {
throw new Error(`Expected auth.success, got: ${JSON.stringify(msg)}`)
}
return msg
}
/**
* Subscribes to an event on an authenticated WebSocket connection.
*/
export function subscribeWs(ws: WebSocket, event: string): void {
sendMessage(ws, {action: 'subscribe', event})
}
/**
* Collects all messages received within a time window.
*/
export function collectMessages(ws: WebSocket, duration: number): Promise<WsMessage[]> {
return new Promise((resolve) => {
const messages: WsMessage[] = []
const handler = (data: WebSocket.Data) => {
messages.push(JSON.parse(data.toString()))
}
ws.on('message', handler)
setTimeout(() => {
ws.off('message', handler)
resolve(messages)
}, duration)
})
}
/**
* Closes a WebSocket connection safely.
*/
export function closeWs(ws: WebSocket): void {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close()
}
}