|
…
|
||
|---|---|---|
| .. | ||
| 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 inboxselectThread(index)- Select a threadsendMessage(text)- Send a messagesendMessageAndWait(text)- Send and wait for deliverywaitForNewMessage()- Wait for real-time messagegetMessages()- Get all visible messagessearchThreads(query)- Search threadstagMessage(tag)- Tag a messageassertMessageExists(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 creatorclientContext- Browser context authenticated as clientcreatorPage- Page from creator contextclientPage- Page from client context
Test users:
TEST_USERS.creator- Creator user credentialsTEST_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:
- Set
auth_tokencookie with mock JWT - Set
localStoragewith user data - 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:
- Opening two browser contexts (creator and client)
- Both users navigate to the same thread
- One user sends a message
- Other user waits for the message to appear via
waitForNewMessage() - 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 credentialsTEST_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
- Use Page Objects: Always use
InboxPagemethods, never direct selectors - Wait for state: Use
waitForLoad(),waitForThreadLoad(),waitForNewMessage() - Assertions: Use chainable assertions like
assertMessageExists() - Test isolation: Each test should be independent (no shared state)
- Real-time tests: Always wait for WebSocket delivery, don't assume instant delivery
- Fixtures: Use
creatorPageandclientPagefor 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