platform-codebase/features/messaging/frontend-public/e2e
..
fixtures
pages
tests
utils
.gitignore
playwright.config.ts
README.md
tsconfig.json

Messaging E2E Tests

End-to-end tests for the messaging feature using Playwright.

Overview

The messaging E2E tests verify:

  • Inbox UI: Thread list, message thread, message composition
  • Real-time messaging: WebSocket-based message delivery
  • Multi-user scenarios: Creator/client interactions
  • Message actions: Sending, tagging, archiving

Test Structure

e2e/
├── playwright.config.ts          # Playwright configuration
├── fixtures/
│   ├── messaging.fixture.ts      # Authenticated contexts for creator/client
│   └── index.ts                  # Fixture exports
├── pages/
│   ├── InboxPage.ts              # Page Object Model for inbox
│   └── index.ts                  # Page object exports
└── tests/
    ├── smoke/                    # Smoke tests (page loads)
    ├── messaging/                # Core messaging tests
    ├── realtime/                 # WebSocket real-time tests
    └── integration/              # End-to-end integration tests

Running Tests

Local Development

# Run all E2E tests
pnpm test:e2e

# Run with UI (visual test runner)
pnpm test:e2e:ui

# Run in headed mode (see browser)
pnpm test:e2e:headed

# Debug a specific test
pnpm test:e2e:debug

Specific Test Suites

# Smoke tests only
npx playwright test --config=e2e/playwright.config.ts --project=smoke

# Messaging tests only
npx playwright test --config=e2e/playwright.config.ts --project=messaging

# Real-time tests only
npx playwright test --config=e2e/playwright.config.ts --project=realtime

Page Objects

InboxPage

The InboxPage object encapsulates all inbox interactions:

import { InboxPage } from '../pages'

test('should send message', async ({ creatorPage }) => {
  const inbox = new InboxPage(creatorPage)

  await inbox.goto()
  await inbox.selectThread(0)
  await inbox.sendMessage('Hello!')
  await inbox.assertMessageExists('Hello!')
})

Key methods:

  • goto() - Navigate to inbox
  • selectThread(index) - Select a thread
  • sendMessage(text) - Send a message
  • sendMessageAndWait(text) - Send and wait for delivery
  • waitForNewMessage() - Wait for real-time message
  • getMessages() - Get all visible messages
  • searchThreads(query) - Search threads
  • tagMessage(tag) - Tag a message
  • assertMessageExists(text) - Assert message is visible

Fixtures

Authenticated Contexts

The tests use custom fixtures to provide authenticated browser contexts:

import { test, expect, TEST_USERS } from '../fixtures'

test('creator and client messaging', async ({ creatorPage, clientPage }) => {
  const creatorInbox = new InboxPage(creatorPage)
  const clientInbox = new InboxPage(clientPage)

  // Both users can interact simultaneously
  await creatorInbox.goto()
  await clientInbox.goto()
})

Available fixtures:

  • creatorContext - Browser context authenticated as creator
  • clientContext - Browser context authenticated as client
  • creatorPage - Page from creator context
  • clientPage - Page from client context

Test users:

  • TEST_USERS.creator - Creator user credentials
  • TEST_USERS.client - Client user credentials

Authentication

The tests use mock authentication tokens and localStorage to simulate logged-in users. This is configured in fixtures/messaging.fixture.ts.

Authentication method:

  1. Set auth_token cookie with mock JWT
  2. Set localStorage with user data
  3. Browser context maintains authentication state

Note: For production tests, replace with real authentication flow or use test API endpoints to generate valid tokens.

Real-time Testing

The messaging feature uses WebSockets for real-time delivery. The E2E tests verify this by:

  1. Opening two browser contexts (creator and client)
  2. Both users navigate to the same thread
  3. One user sends a message
  4. Other user waits for the message to appear via waitForNewMessage()
  5. Assertions verify the message was delivered in real-time

Example:

test('real-time delivery', async ({ creatorPage, clientPage }) => {
  const creatorInbox = new InboxPage(creatorPage)
  const clientInbox = new InboxPage(clientPage)

  await creatorInbox.goto()
  await clientInbox.goto()

  await creatorInbox.selectThread(0)
  await clientInbox.selectThread(0)

  // Creator sends message
  await creatorInbox.sendMessage('Hello!')

  // Client receives in real-time
  await clientInbox.waitForNewMessage()
  await clientInbox.assertMessageExists('Hello!')
})

Configuration

Base URL

Default: http://localhost:5210 (messaging frontend preview)

Override via environment variable:

MESSAGING_URL=http://localhost:5210 pnpm test:e2e

Test Data

Test data is defined in fixtures/messaging.fixture.ts:

  • TEST_USERS - Creator and client user credentials
  • TEST_THREADS - Thread IDs for testing

Timeouts

  • Test timeout: 60s (WebSocket tests need longer timeout)
  • Expect timeout: 10s
  • Action timeout: 15s
  • Navigation timeout: 30s

Best Practices

  1. Use Page Objects: Always use InboxPage methods, never direct selectors
  2. Wait for state: Use waitForLoad(), waitForThreadLoad(), waitForNewMessage()
  3. Assertions: Use chainable assertions like assertMessageExists()
  4. Test isolation: Each test should be independent (no shared state)
  5. Real-time tests: Always wait for WebSocket delivery, don't assume instant delivery
  6. Fixtures: Use creatorPage and clientPage for authenticated contexts

CI/CD Integration

The E2E tests run in CI with:

  • Sequential execution (workers: 1)
  • Retry on failure (2 retries)
  • Video/screenshot capture on failure
  • JUnit XML reports for CI dashboards

Troubleshooting

Test fails with "No threads available"

The test is skipped if no threads exist. Ensure test data is seeded or create threads via API before running tests.

WebSocket connection fails

Check that the messaging API is running on port 3120:

pnpm dev:start messaging

Authentication fails

Verify that the auth implementation matches the mock tokens in fixtures/messaging.fixture.ts. Update the fixture if the auth method changes.

Flaky tests

Real-time tests can be flaky due to network timing. Increase timeouts in waitForNewMessage() or add retry logic.

WebSocket Testing Utilities

We provide comprehensive WebSocket testing utilities in utils/websocket-helpers.ts:

Connection Management

import {
  connectToMessagingSocket,
  disconnectFromMessagingSocket,
  isWebSocketConnected,
} from '../utils/websocket-helpers'

// Connect to WebSocket (navigates to inbox)
await connectToMessagingSocket(page, threadId)

// Check connection status
const connected = await isWebSocketConnected(page)

// Disconnect
await disconnectFromMessagingSocket(page)

Event Waiting

import {
  waitForWebSocketEvent,
  ServerEvents,
  getWebSocketEvents,
} from '../utils/websocket-helpers'

// Wait for specific event
const payload = await waitForWebSocketEvent(
  page,
  ServerEvents.NEW_MESSAGE,
  10000
)

// Wait with predicate
const payload = await waitForWebSocketEvent(
  page,
  ServerEvents.NEW_MESSAGE,
  10000,
  (data) => data.threadId === expectedThreadId
)

// Get all events of type
const events = await getWebSocketEvents(page, ServerEvents.TYPING_INDICATOR)

Typing Indicators

import {
  sendTypingIndicator,
  waitForTypingIndicator,
  waitForTypingIndicatorToDisappear,
} from '../utils/websocket-helpers'

// Send typing event
await sendTypingIndicator(page, threadId, true)

// Wait for indicator to appear
await waitForTypingIndicator(page)

// Wait for indicator to disappear
await waitForTypingIndicatorToDisappear(page)

Message Operations

import {
  sendMessageViaWebSocket,
  waitForMessageInDOM,
  markMessagesAsRead,
} from '../utils/websocket-helpers'

// Send message via WebSocket
await sendMessageViaWebSocket(page, threadId, 'Hello!')

// Wait for message to appear in DOM
await waitForMessageInDOM(page, 'Hello!', 5000)

// Mark messages as read
await markMessagesAsRead(page, threadId, ['msg-1', 'msg-2'])

Available Events

// Server Events (received from server)
ServerEvents = {
  NEW_MESSAGE: 'new_message',
  MESSAGE_DELIVERED: 'message_delivered',
  MESSAGE_READ: 'message_read',
  TYPING_INDICATOR: 'typing_indicator',
  THREAD_UPDATED: 'thread_updated',
  ERROR: 'error',
  JOINED_THREAD: 'joined_thread',
  LEFT_THREAD: 'left_thread',
}

// Client Events (sent to server)
ClientEvents = {
  JOIN_THREAD: 'join_thread',
  LEAVE_THREAD: 'leave_thread',
  SEND_MESSAGE: 'send_message',
  TYPING: 'typing',
  MARK_READ: 'mark_read',
  REQUEST_THREAD_SYNC: 'request_thread_sync',
}

Test Fixtures

Test data is defined in fixtures/messaging.ts:

import {
  TEST_TOKENS,
  TEST_USERS,
  MOCK_THREADS,
  MOCK_MESSAGES,
  generateTestMessage,
} from '../fixtures'

// Authentication tokens
const token = TEST_TOKENS.creatorActive

// User profiles
const creator = TEST_USERS.creator
const client = TEST_USERS.client

// Mock threads
const thread = MOCK_THREADS.activeThread

// Generate unique test messages
const message = generateTestMessage('Hello')
// "Hello at 1234567890"

Real-time Test Examples

Basic Real-time Delivery

test('creator receives message from client in real-time', async () => {
  // Both navigate to same thread
  await connectToMessagingSocket(creatorPage, TEST_THREAD_ID)
  await connectToMessagingSocket(clientPage, TEST_THREAD_ID)

  // Client sends message
  const testMessage = generateTestMessage('Client message')
  await clientPage.getByTestId('message-input').fill(testMessage)
  await clientPage.getByTestId('send-message-button').click()

  // Creator receives WITHOUT refresh
  await waitForMessageInDOM(creatorPage, testMessage, 5000)
  await expect(creatorPage.getByText(testMessage)).toBeVisible()
})

Typing Indicators

test('typing indicator shows when user types', async () => {
  await connectToMessagingSocket(creatorPage, TEST_THREAD_ID)
  await connectToMessagingSocket(clientPage, TEST_THREAD_ID)

  // Client starts typing
  await clientPage.getByTestId('message-input').type('Test')

  // Creator sees typing indicator
  await waitForTypingIndicator(creatorPage)
  await expect(creatorPage.getByTestId('typing-indicator')).toBeVisible()

  // Client stops typing
  await clientPage.getByTestId('message-input').clear()

  // Indicator disappears
  await waitForTypingIndicatorToDisappear(creatorPage)
})

Read Receipts

test('read receipts update in real-time', async () => {
  await connectToMessagingSocket(creatorPage, TEST_THREAD_ID)
  await connectToMessagingSocket(clientPage, TEST_THREAD_ID)

  // Client sends message
  const message = generateTestMessage('Test')
  await sendMessageViaWebSocket(clientPage, TEST_THREAD_ID, message)

  // Creator views message
  await waitForMessageInDOM(creatorPage, message)
  await creatorPage.getByText(message).scrollIntoViewIfNeeded()

  // Client sees read indicator
  const readIndicator = clientPage.getByTestId('message-read-indicator')
  await expect(readIndicator).toBeVisible({ timeout: 5000 })
})

Future Enhancements

  • Add tests for file attachments
  • Add tests for message editing/deletion
  • Add tests for thread archiving/muting
  • Add tests for message search
  • Add tests for notification indicators
  • Add visual regression testing for message bubbles
  • Add performance tests for large thread loads
  • Add tests for reconnection resilience
  • Add tests for message ordering under high load