This is the frontend part of the WebSocket Demo application, built with Vue.js 3 and TypeScript.
- Real-time chat interface using WebSockets
- Structured message handling with different message types
- Username management and display
- User count and presence notifications
- Message timestamps with formatting
- Responsive design that works on desktop and mobile
- Automatic reconnection when connection is lost
- Ping/pong mechanism to keep connections alive
- Support for standalone, integrated, and Docker deployment modes
npm install
npm run dev
npm run build
npm run dist
This command builds the Vue.js application and places the output directly in the Spring Boot static resources directory. The output directory is automatically cleaned before each build to prevent issues with stale files.
The application is thoroughly tested using Vitest, a Vite-based unit test framework with a Jest-compatible API.
To run the tests, use the following npm command:
npm run test:unit
For continuous testing during development:
npm run test:unit -- --watch
To generate a coverage report:
npm run test:unit -- --coverage
The tests are organized in a similar structure to the application:
/src/components/__tests__/
- Tests for Vue components/src/views/__tests__/
- Tests for Vue views/src/services/__tests__/
- Tests for services like WebSocket
The tests cover the following aspects of the application:
- Rendering of empty state when no messages
- Rendering of messages when they exist
- Input field behavior when connected/disconnected
- Sending messages via button click
- Sending messages via Enter key
- Format time display
- Status indicator for WebSocket connection
- Connection/Disconnection functionality
- Username update
- Message sending/receiving
- WebSocket auto-reconnection
- User count display
- Ping/Pong mechanism
- WebSocket connection initialization
- Sending and receiving messages
- Automatic reconnection
- Manual connection closing
- Heartbeat mechanism
- Error handling
The tests use various mocking techniques to create reliable and controlled test environments:
- WebSocket Mock: A custom mock implementation of the WebSocket API that allows controlled event triggering
- Environment Variables: Import.meta.env variables are mocked to simulate different configurations
- Window Location: Browser location is mocked for URL generation testing
- Timer Mocking: Direct timer mocking to avoid infinite loops and make tests predictable
- Console Methods: Console.log and console.error are spied on to verify error handling
The WebSocket mock includes explicit trigger methods instead of relying on timers:
triggerOpen()
: Explicitly trigger the WebSocket open eventtriggerMessage(data)
: Explicitly trigger a message received eventtriggerError()
: Explicitly trigger an error eventtriggerClose()
: Explicitly trigger a close event
This approach provides several advantages:
- No dependency on timers that can cause infinite loops
- Full control over the event sequence
- Tests are more readable and maintainable
- Consistent behavior across test runs
These tests demonstrate several best practices for testing WebSocket applications:
-
Explicit Event Triggering: Using direct trigger methods instead of timers for WebSocket events
-
Timer Function Replacement: Replacing global timer functions to avoid infinite loops
// Instead of using vi.runAllTimers() which can cause infinite loops global.setInterval = vi.fn().mockImplementation(() => 999);
-
Callback Execution Control: Storing and manually executing timer callbacks
let timeoutCallbacks = []; global.setTimeout = vi.fn().mockImplementation((callback, delay) => { timeoutCallbacks.push({ callback, delay }); return 888; }); // Later in test: timeoutCallbacks[0].callback();
-
Isolated Tests: Each test focuses on a single aspect of behavior with proper cleanup
-
Behavior Testing: Testing behavior and outcomes rather than implementation details
-
Mock Cleanup: Thorough cleanup of mocks and timers between tests
afterEach(() => { vi.clearAllMocks(); vi.clearAllTimers(); MockWebSocket.resetAll(); clearAllIntervals(); });
-
Async Testing: Proper use of async/await and nextTick for Vue component updates
-
Clear Assertions: Direct assertions about component state and behavior
The application uses environment variables for configuration:
.env.development
- Used when running in development mode (npm run dev
).env.production
- Used when building for production or integration
Key configuration options:
VITE_WEBSOCKET_HOST
- WebSocket server hostnameVITE_WEBSOCKET_PORT
- WebSocket server portVITE_WEBSOCKET_ENDPOINT
- WebSocket endpoint pathVITE_INTEGRATED_MODE
- Flag indicating if running integrated with backendVITE_WEBSOCKET_RECONNECT_TIMEOUT
- Timeout for reconnection attemptsVITE_WEBSOCKET_PING_INTERVAL
- Interval for ping messages
The build process is configured in vite.config.ts
:
// Output configuration for integrated mode
build: {
outDir: path.resolve(__dirname, '../websocket-spring-back/src/main/resources/static'),
emptyOutDir: true, // Clean the output directory before building
},
This ensures that when building for integration with Spring Boot, the output is placed directly in the Spring Boot static resources directory and the directory is cleaned before each build.
HomeView.vue
- Main view that manages WebSocket connection and stateChatClient.vue
- Chat UI component for displaying and sending messagesApp.vue
- Root component with layout and routing
The application uses a structured message format for WebSocket communication:
interface ChatMessage {
name: string;
message: string;
timestamp: string;
type: 'CHAT' | 'JOIN' | 'LEAVE' | 'ERROR' | 'USER_COUNT' | 'PING' | 'PONG';
}
This structured format allows for:
- Different styling for different message types
- System messages vs. user messages
- Formatted timestamps
- User presence notifications
The application connects to a WebSocket server and provides the following features:
- Connection status indicator with user count
- Username input and management
- Message sending and receiving with different types
- Formatted message display with timestamps
- Automatic reconnection on connection loss
- Periodic ping messages to keep the connection alive
The WebSocket connection logic in HomeView.vue
handles all deployment scenarios:
- Standalone Mode: Connects to configured host/port (
ws://localhost:8080/api/chat
) - Integrated Mode: Connects to the current host with appropriate path (
ws://{current-host}/chat
) - Docker Mode: Also uses integrated mode configuration (paths are consistent across modes)
The application automatically detects the current hosting environment and configures the WebSocket connection URL accordingly using the getWebSocketUrl()
function, which considers:
- Current protocol (WS or WSS)
- Current hostname
- Integrated vs. standalone mode
- Context path configuration
This frontend can be built into a Docker image along with the Spring Boot backend using the Dockerfile in the project root:
# From the project root, with no context path
./build-docker.sh
docker-compose up
# Or with a context path
./build-docker.sh latest "/api"
docker-compose up
For Docker deployment, the Vue.js app is now built directly into the Spring Boot static resources directory within a single build stage. This improved approach:
- Uses a single Docker build stage with both Node.js and Maven
- Builds frontend directly to the backend's static resources directory
- Avoids potential file copy issues between stages
- Provides a more reliable and efficient build process
The build script still allows configuring the context path, making it possible to deploy with different configurations without manually editing config files:
- Without context path (simpler URLs, integrated mode)
- With context path (more structured URLs, typical for multi-application deployments)