Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit bb1c288

Browse files
committed
Fix e2e testing
1 parent 1df07c2 commit bb1c288

File tree

11 files changed

+160
-82
lines changed

11 files changed

+160
-82
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949
5050
- name: Lint
5151
run: pnpm --filter client lint
52+
53+
- name: End-to-end tests
54+
run: pnpm e2e
5255

5356
- name: Build
5457
run: pnpm build

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
26+
playwright-report
27+
playwright-report/index.html
28+
test-results

e2e/test-1.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test('should add new todo item', async ({ page }) => {
4+
await page.goto('http://localhost:5173/');
5+
await page.getByRole('textbox', { name: 'Add a new todo' }).click();
6+
await page.getByRole('textbox', { name: 'Add a new todo' }).fill('Hello');
7+
await page.getByRole('button', { name: 'Add' }).click();
8+
await expect(page.getByRole('list')).toContainText('Hello');
9+
});
10+
11+
test('should mark a todo as completed', async ({ page }) => {
12+
await page.goto('http://localhost:5173');
13+
14+
// Add a new todo if none exists
15+
if (await page.locator('li').count() === 0) {
16+
await page.getByRole('textbox', { name: 'Add a new todo' }).click();
17+
await page.getByRole('textbox', { name: 'Add a new todo' }).fill('Hello');
18+
await page.getByRole('button', { name: 'Add' }).click();
19+
}
20+
21+
// Mark the first todo as completed
22+
await page.click('input[type="checkbox"]');
23+
24+
// Verify the todo is marked as completed
25+
await expect(page.locator('li span:has-text("Hello")').first()).toHaveCSS('text-decoration', /line-through/);
26+
});

e2e/todo.spec.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
"typecheck": "pnpm --filter shared build && pnpm --filter server exec tsc --noEmit"
1616
},
1717
"devDependencies": {
18-
"typescript": "~5.7.2",
19-
"husky": "^9.0.6",
20-
"lint-staged": "^15.2.0",
18+
"@eslint/js": "^9.22.0",
2119
"@playwright/test": "^1.40.0",
20+
"@types/node": "^18.15.11",
2221
"eslint": "^9.22.0",
23-
"@eslint/js": "^9.22.0",
24-
"globals": "^16.0.0",
25-
"typescript-eslint": "^8.26.1",
2622
"eslint-plugin-react-hooks": "^5.2.0",
27-
"eslint-plugin-react-refresh": "^0.4.19"
23+
"eslint-plugin-react-refresh": "^0.4.19",
24+
"globals": "^16.0.0",
25+
"husky": "^9.0.6",
26+
"lint-staged": "^15.2.0",
27+
"typescript": "~5.7.2",
28+
"typescript-eslint": "^8.26.1"
2829
},
2930
"lint-staged": {
3031
"*.{ts,tsx}": [

packages/client/src/App.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,21 @@ function App() {
1010
const [input, setInput] = useState("");
1111
const [loading, setLoading] = useState(true);
1212
const [error, setError] = useState<string | null>(null);
13+
const [errorCount, setErrorCount] = useState(1);
1314
const [onlineUsers, setOnlineUsers] = useState<number>(1);
1415

16+
// Function to handle errors with counter
17+
const handleError = (errorMessage: string) => {
18+
if (error === errorMessage) {
19+
// Same error occurred again, increment counter
20+
setErrorCount(prev => prev + 1);
21+
} else {
22+
// New error, reset counter and set new error
23+
setError(errorMessage);
24+
setErrorCount(1);
25+
}
26+
};
27+
1528
// Calculate progress
1629
const totalTodos = todos.length;
1730
const completedTodos = todos.filter(todo => todo.completed).length;
@@ -27,8 +40,9 @@ function App() {
2740
// Check if the response is an object with a todos property
2841
setTodos(Array.isArray(data) ? data : (data.todos || []));
2942
setError(null);
43+
setErrorCount(1);
3044
} catch (err) {
31-
setError("Failed to fetch todos. Please try again.");
45+
handleError("Failed to fetch todos. Please try again.");
3246
console.error(err);
3347
} finally {
3448
setLoading(false);
@@ -41,20 +55,28 @@ function App() {
4155
socketService.connect();
4256

4357
// Listen for todo updates
44-
const unsubscribe = socketService.on('todos:update', (updatedTodos: Todo[]) => {
45-
setTodos(updatedTodos);
58+
const unsubscribeTodos = socketService.on('todos:update', (updatedTodos: Todo[] | { todos: Todo[] }) => {
59+
console.log('Received todos update:', updatedTodos);
60+
if (Array.isArray(updatedTodos)) {
61+
setTodos(updatedTodos);
62+
} else if (updatedTodos && 'todos' in updatedTodos) {
63+
setTodos(updatedTodos.todos);
64+
}
4665
});
4766

4867
// Listen for user count updates
49-
socketService.on('users:count', (count: number) => {
68+
const unsubscribeUsers = socketService.on('users:count', (count: number) => {
69+
console.log('Received users count update:', count);
5070
setOnlineUsers(count);
5171
});
5272

5373
// Cleanup on unmount
5474
return () => {
55-
unsubscribe();
75+
unsubscribeTodos();
76+
unsubscribeUsers();
5677
socketService.disconnect();
5778
};
79+
// eslint-disable-next-line react-hooks/exhaustive-deps
5880
}, []);
5981

6082
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -64,7 +86,7 @@ function App() {
6486
await createTodo(input.trim());
6587
setInput("");
6688
} catch (err) {
67-
setError("Failed to create todo. Please try again.");
89+
handleError("Failed to create todo. Please try again.");
6890
console.error(err);
6991
}
7092
}
@@ -86,7 +108,7 @@ function App() {
86108

87109
await updateTodo(id, { completed: !todoToUpdate.completed });
88110
} catch (err) {
89-
setError("Failed to update todo. Please try again.");
111+
handleError("Failed to update todo. Please try again.");
90112
console.error(err);
91113
}
92114
};
@@ -95,7 +117,7 @@ function App() {
95117
try {
96118
await deleteTodo(id);
97119
} catch (err) {
98-
setError("Failed to delete todo. Please try again.");
120+
handleError("Failed to delete todo. Please try again.");
99121
console.error(err);
100122
}
101123
};
@@ -137,8 +159,13 @@ function App() {
137159
<h2 className="text-xl font-bold mb-4 text-white">Your Tasks</h2>
138160

139161
{error && (
140-
<div className="bg-red-900/50 border border-red-700 text-white p-3 rounded mb-4">
141-
{error}
162+
<div className="bg-red-900/50 border border-red-700 text-white p-3 rounded mb-4 flex justify-between items-center">
163+
<span>{error}</span>
164+
{errorCount > 1 && (
165+
<span className="bg-red-700 text-white text-xs font-medium px-2 py-1 rounded-full">
166+
{errorCount}
167+
</span>
168+
)}
142169
</div>
143170
)}
144171

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,65 @@
1-
// Basic socket service
2-
const socketService = {
3-
connect: () => {
4-
// Implementation will be mocked in tests
5-
},
6-
disconnect: () => {
7-
// Implementation will be mocked in tests
8-
},
9-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
10-
on: (_event: string, _callback: (...args: any[]) => void) => {
11-
// Implementation will be mocked in tests
12-
return () => {}; // Unsubscribe function
13-
},
14-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
15-
emit: (_event: string, ..._args: any[]) => {
16-
// Implementation will be mocked in tests
17-
},
18-
isTodoValid: (id: string): boolean => {
19-
// Simple implementation to validate todo IDs
1+
import { io, Socket } from 'socket.io-client';
2+
3+
// Get the server URL from environment or use default
4+
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3001';
5+
6+
class SocketService {
7+
private socket: Socket | null = null;
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
private eventHandlers: Record<string, Array<(...args: any[]) => void>> = {};
10+
11+
connect(): void {
12+
if (this.socket) return;
13+
14+
this.socket = io(SERVER_URL);
15+
16+
this.socket.on('connect', () => {
17+
console.log('Socket connected:', this.socket?.id);
18+
});
19+
20+
this.socket.on('connect_error', (error) => {
21+
console.error('Socket connection error:', error);
22+
});
23+
24+
// Re-register any existing event handlers after reconnection
25+
Object.entries(this.eventHandlers).forEach(([event, handlers]) => {
26+
handlers.forEach(handler => {
27+
this.socket?.on(event, handler);
28+
});
29+
});
30+
}
31+
32+
disconnect(): void {
33+
if (!this.socket) return;
34+
35+
this.socket.disconnect();
36+
this.socket = null;
37+
console.log('Socket disconnected');
38+
}
39+
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
on(event: string, callback: (...args: any[]) => void): () => void {
42+
if (!this.eventHandlers[event]) {
43+
this.eventHandlers[event] = [];
44+
}
45+
46+
this.eventHandlers[event].push(callback);
47+
this.socket?.on(event, callback);
48+
49+
return () => {
50+
this.eventHandlers[event] = this.eventHandlers[event].filter(cb => cb !== callback);
51+
this.socket?.off(event, callback);
52+
};
53+
}
54+
55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
emit(event: string, ...args: any[]): void {
57+
this.socket?.emit(event, ...args);
58+
}
59+
60+
isTodoValid(id: string): boolean {
2061
return id !== undefined && typeof id === 'string' && id.length > 0;
2162
}
22-
};
63+
}
2364

24-
export default socketService;
65+
export default new SocketService();

packages/server/src/index.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ io.on('connection', (socket) => {
6767
io.emit('users:count', onlineUsers);
6868

6969
// Send current todos to newly connected client
70-
socket.emit('todos:init', todos);
70+
socket.emit('todos:update', todos);
7171

7272
socket.on('disconnect', () => {
7373
console.info('Client disconnected:', socket.id);
@@ -209,16 +209,11 @@ const broadcastTodos = () => {
209209

210210
// Debounce broadcasts to reduce frequency
211211
setTimeout(() => {
212-
// Only send the first 100 todos to reduce payload size
213-
const limitedTodos = todos.slice(0, 100);
212+
// Send the full todos list to all clients
213+
io.emit('todos:update', todos);
214214

215-
// Send the todo IDs separately to help clients track what's available
216-
const todoIds = todos.map(t => t.id);
217-
218-
io.emit('todos:update', limitedTodos);
219-
io.emit('todos:ids', todoIds);
220215
broadcastPending = false;
221-
}, 100);
216+
}, 50); // Reduced debounce time for faster updates
222217
};
223218

224219
// Add memory usage monitoring

packages/server/src/stress-test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import fs from 'fs';
77

88
const API_URL = 'http://localhost:3001/api';
99
const SOCKET_URL = 'ws://localhost:3001';
10-
const NUM_USERS = 500; // Reduced from 100
11-
const OPERATIONS_PER_USER = 30; // Reduced from 20
12-
const DELAY_BETWEEN_OPS_MS = 100; // Increased from 200
10+
const NUM_USERS = 600; // Reduced from 100
11+
const OPERATIONS_PER_USER = 60; // Reduced from 20
12+
const DELAY_BETWEEN_OPS_MS = 20; // Increased from 200
1313
const LOG_FILE = 'stress-test-results.log';
1414
const BATCH_SIZE = 20;
1515

@@ -52,7 +52,12 @@ class User {
5252

5353
constructor(id: number) {
5454
this.id = id;
55-
this.socket = io(SOCKET_URL);
55+
this.socket = io(SOCKET_URL, {
56+
transports: ['websocket'],
57+
reconnection: true,
58+
reconnectionAttempts: 5,
59+
reconnectionDelay: 1000
60+
});
5661

5762
this.socket.on('connect', () => {
5863
conditionalLog(`User ${this.id} connected with socket ID: ${this.socket.id}`);
@@ -63,13 +68,14 @@ class User {
6368
console.error(`User ${this.id} socket connection error:`, error.message);
6469
});
6570

66-
// Update to handle the new todos format (paginated)
71+
// Update to handle the new todos format (paginated or array)
6772
this.socket.on('todos:update', (todosData: Todo[] | { todos: Todo[] }) => {
6873
if (Array.isArray(todosData)) {
6974
this.todos = todosData;
7075
} else if (todosData && 'todos' in todosData) {
7176
this.todos = todosData.todos;
7277
}
78+
conditionalLog(`User ${this.id} received ${this.todos.length} todos`);
7379
});
7480
}
7581

playwright.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default defineConfig({
88
workers: process.env.CI ? 1 : undefined,
99
reporter: 'html',
1010
use: {
11-
baseURL: 'http://localhost:3000',
11+
baseURL: 'http://localhost:5173',
1212
trace: 'on-first-retry',
1313
},
1414
projects: [
@@ -33,7 +33,7 @@ export default defineConfig({
3333
},
3434
{
3535
command: 'pnpm --filter client dev',
36-
port: 3000,
36+
port: 5173,
3737
reuseExistingServer: !process.env.CI,
3838
},
3939
],

0 commit comments

Comments
 (0)