create-elit 3.3.2 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/README.md +1 -0
- package/dist/templates/elit.config.ts +16 -0
- package/dist/templates/package.json +4 -1
- package/dist/templates/src/client.test.ts +292 -0
- package/dist/templates/src/components/Footer.test.ts +226 -0
- package/dist/templates/src/components/Header.test.ts +493 -0
- package/dist/templates/src/pages/ChatListPage.test.ts +603 -0
- package/dist/templates/src/pages/ChatPage.test.ts +530 -0
- package/dist/templates/src/pages/ForgotPasswordPage.test.ts +484 -0
- package/dist/templates/src/pages/HomePage.test.ts +601 -0
- package/dist/templates/src/pages/LoginPage.test.ts +619 -0
- package/dist/templates/src/pages/PrivateChatPage.test.ts +556 -0
- package/dist/templates/src/pages/ProfilePage.test.ts +628 -0
- package/dist/templates/src/pages/RegisterPage.test.ts +661 -0
- package/package.json +1 -1
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatListPage Component Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// CRITICAL: Set up ALL mocks BEFORE importing ChatListPage component
|
|
6
|
+
// The component reads localStorage during import, so mocks must be set up first
|
|
7
|
+
|
|
8
|
+
// Simple mock function to track calls
|
|
9
|
+
function mockFn() {
|
|
10
|
+
const calls: any[] = [];
|
|
11
|
+
const fn = (...args: any[]) => {
|
|
12
|
+
calls.push(args);
|
|
13
|
+
return undefined;
|
|
14
|
+
};
|
|
15
|
+
fn.calls = calls;
|
|
16
|
+
fn.mockClear = () => {
|
|
17
|
+
calls.length = 0;
|
|
18
|
+
};
|
|
19
|
+
return fn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mock localStorage
|
|
23
|
+
const localStorageMock = (() => {
|
|
24
|
+
let store: Record<string, string> = {};
|
|
25
|
+
return {
|
|
26
|
+
getItem: (key: string): string | null => store[key] || null,
|
|
27
|
+
setItem: (key: string, value: string): void => {
|
|
28
|
+
store[key] = value;
|
|
29
|
+
},
|
|
30
|
+
removeItem: (key: string): void => {
|
|
31
|
+
delete store[key];
|
|
32
|
+
},
|
|
33
|
+
clear: (): void => {
|
|
34
|
+
store = {};
|
|
35
|
+
},
|
|
36
|
+
get length(): number {
|
|
37
|
+
return Object.keys(store).length;
|
|
38
|
+
},
|
|
39
|
+
key: (index: number): string | null => {
|
|
40
|
+
return Object.keys(store)[index] || null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
// Mock fetch
|
|
46
|
+
let mockFetchResponse: any = {
|
|
47
|
+
ok: true,
|
|
48
|
+
json: async () => ({ users: [] })
|
|
49
|
+
};
|
|
50
|
+
const mockFetch = async () => mockFetchResponse;
|
|
51
|
+
|
|
52
|
+
// Mock window.addEventListener
|
|
53
|
+
const eventListeners: Record<string, Function[]> = {};
|
|
54
|
+
const addEventListenerMock = (event: string, handler: Function) => {
|
|
55
|
+
if (!eventListeners[event]) {
|
|
56
|
+
eventListeners[event] = [];
|
|
57
|
+
}
|
|
58
|
+
eventListeners[event].push(handler);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Mock requestAnimationFrame
|
|
62
|
+
let rafId = 0;
|
|
63
|
+
const rafCallbacks: Map<number, FrameRequestCallback> = new Map();
|
|
64
|
+
const mockRequestAnimationFrame = (callback: FrameRequestCallback) => {
|
|
65
|
+
const id = ++rafId;
|
|
66
|
+
rafCallbacks.set(id, callback);
|
|
67
|
+
return id;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mockCancelAnimationFrame = (id: number) => {
|
|
71
|
+
rafCallbacks.delete(id);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Trigger a RAF callback
|
|
75
|
+
const triggerRAF = () => {
|
|
76
|
+
for (const [id, callback] of rafCallbacks) {
|
|
77
|
+
try {
|
|
78
|
+
callback(performance.now() as any);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// Ignore errors
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
rafCallbacks.clear();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Mock router
|
|
87
|
+
const mockRouter = {
|
|
88
|
+
push: mockFn() as any,
|
|
89
|
+
replace: mockFn() as any,
|
|
90
|
+
go: mockFn() as any,
|
|
91
|
+
back: mockFn() as any,
|
|
92
|
+
forward: mockFn() as any,
|
|
93
|
+
currentPath: '/',
|
|
94
|
+
currentState: null
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// SETUP GLOBALS BEFORE IMPORT
|
|
98
|
+
// Clear real localStorage if it exists
|
|
99
|
+
if (typeof localStorage !== 'undefined') {
|
|
100
|
+
localStorage.clear();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set up global localStorage mock
|
|
104
|
+
(global as any).localStorage = localStorageMock;
|
|
105
|
+
(globalThis as any).localStorage = localStorageMock;
|
|
106
|
+
|
|
107
|
+
// Set up global fetch mock
|
|
108
|
+
(global as any).fetch = mockFetch;
|
|
109
|
+
(globalThis as any).fetch = mockFetch;
|
|
110
|
+
|
|
111
|
+
// Set up window mock
|
|
112
|
+
(global as any).window = {
|
|
113
|
+
addEventListener: addEventListenerMock,
|
|
114
|
+
removeEventListener: mockFn(),
|
|
115
|
+
localStorage: localStorageMock
|
|
116
|
+
};
|
|
117
|
+
(globalThis as any).window = (global as any).window;
|
|
118
|
+
|
|
119
|
+
// Set up requestAnimationFrame mocks
|
|
120
|
+
(global as any).requestAnimationFrame = mockRequestAnimationFrame;
|
|
121
|
+
(global as any).cancelAnimationFrame = mockCancelAnimationFrame;
|
|
122
|
+
(globalThis as any).requestAnimationFrame = mockRequestAnimationFrame;
|
|
123
|
+
(globalThis as any).cancelAnimationFrame = mockCancelAnimationFrame;
|
|
124
|
+
|
|
125
|
+
// NOW import the component (after mocks are set up)
|
|
126
|
+
import { ChatListPage } from './ChatListPage';
|
|
127
|
+
import type { VNode } from 'elit/types';
|
|
128
|
+
|
|
129
|
+
// Helper function to render VNode to HTML string
|
|
130
|
+
function renderToString(vNode: VNode | string | number | undefined | null): string {
|
|
131
|
+
if (vNode == null || vNode === false) return '';
|
|
132
|
+
if (typeof vNode !== 'object') return String(vNode);
|
|
133
|
+
|
|
134
|
+
const { tagName, props, children } = vNode;
|
|
135
|
+
const attrs = props ? Object.entries(props)
|
|
136
|
+
.filter(([k, v]) => v != null && v !== false && k !== 'children' && k !== 'ref' && !k.startsWith('on'))
|
|
137
|
+
.map(([k, v]) => {
|
|
138
|
+
if (k === 'className' || k === 'class') return `class="${Array.isArray(v) ? v.join(' ') : v}"`;
|
|
139
|
+
if (k === 'style') return `style="${typeof v === 'string' ? v : Object.entries(v).map(([sk, sv]) => `${sk.replace(/([A-Z])/g, '-$1').toLowerCase()}:${sv}`).join(';')}"`;
|
|
140
|
+
if (v === true) return k;
|
|
141
|
+
return `${k}="${v}"`;
|
|
142
|
+
})
|
|
143
|
+
.join(' ') : '';
|
|
144
|
+
|
|
145
|
+
const childrenStr = children && children.length > 0
|
|
146
|
+
? children.map(c => renderToString(c as any)).join('')
|
|
147
|
+
: '';
|
|
148
|
+
|
|
149
|
+
return `<${tagName}${attrs ? ' ' + attrs : ''}>${childrenStr}</${tagName}>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Helper function to find child by tag name
|
|
153
|
+
function findChildByTagName(vNode: VNode, tagName: string): VNode | null {
|
|
154
|
+
if (vNode && typeof vNode === 'object' && 'tagName' in vNode) {
|
|
155
|
+
if (vNode.tagName === tagName) {
|
|
156
|
+
return vNode;
|
|
157
|
+
}
|
|
158
|
+
if (vNode.children) {
|
|
159
|
+
for (const child of vNode.children) {
|
|
160
|
+
if (typeof child === 'object' && child !== null) {
|
|
161
|
+
const found = findChildByTagName(child as VNode, tagName);
|
|
162
|
+
if (found) return found;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Helper function to find children by tag name
|
|
171
|
+
function findChildrenByTagName(vNode: VNode, tagName: string): VNode[] {
|
|
172
|
+
const results: VNode[] = [];
|
|
173
|
+
|
|
174
|
+
function search(node: VNode | string | number | null | undefined) {
|
|
175
|
+
if (node && typeof node === 'object' && 'tagName' in node) {
|
|
176
|
+
if (node.tagName === tagName) {
|
|
177
|
+
results.push(node);
|
|
178
|
+
}
|
|
179
|
+
if (node.children) {
|
|
180
|
+
for (const child of node.children) {
|
|
181
|
+
search(child as any);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
search(vNode);
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
describe('ChatListPage Component', () => {
|
|
192
|
+
// Clear REAL browser localStorage before any tests run
|
|
193
|
+
beforeAll(() => {
|
|
194
|
+
if (typeof localStorage !== 'undefined') {
|
|
195
|
+
localStorage.clear();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
// Clear RAF callbacks FIRST
|
|
201
|
+
rafCallbacks.clear();
|
|
202
|
+
rafId = 0;
|
|
203
|
+
|
|
204
|
+
// Clear localStorage before each test
|
|
205
|
+
localStorageMock.clear();
|
|
206
|
+
|
|
207
|
+
// Also clear REAL browser localStorage
|
|
208
|
+
if (typeof localStorage !== 'undefined') {
|
|
209
|
+
localStorage.clear();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Reset mock router
|
|
213
|
+
(mockRouter.push as any).mockClear();
|
|
214
|
+
(mockRouter.replace as any).mockClear();
|
|
215
|
+
|
|
216
|
+
// Reset fetch mock
|
|
217
|
+
mockFetchResponse = {
|
|
218
|
+
ok: true,
|
|
219
|
+
json: async () => ({ users: [] })
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Clear event listeners
|
|
223
|
+
Object.keys(eventListeners).forEach(key => {
|
|
224
|
+
delete eventListeners[key];
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
afterEach(() => {
|
|
229
|
+
// Clean up event listeners after each test
|
|
230
|
+
Object.keys(eventListeners).forEach(key => {
|
|
231
|
+
delete eventListeners[key];
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Clear RAF callbacks after each test
|
|
235
|
+
rafCallbacks.clear();
|
|
236
|
+
rafId = 0;
|
|
237
|
+
|
|
238
|
+
// Clear REAL browser localStorage after each test
|
|
239
|
+
if (typeof localStorage !== 'undefined') {
|
|
240
|
+
localStorage.clear();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('authentication', () => {
|
|
245
|
+
it('should render page when authenticated', () => {
|
|
246
|
+
// Set both token and user
|
|
247
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
248
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
249
|
+
|
|
250
|
+
const page = ChatListPage(mockRouter as any);
|
|
251
|
+
|
|
252
|
+
expect(page).toBeDefined();
|
|
253
|
+
expect(page.tagName).toBe('div');
|
|
254
|
+
expect(page.props?.className).toBe('chat-list-page');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should not redirect when authenticated', () => {
|
|
258
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
259
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
260
|
+
|
|
261
|
+
ChatListPage(mockRouter as any);
|
|
262
|
+
|
|
263
|
+
// Should not redirect when authenticated
|
|
264
|
+
expect(mockRouter.push.calls.length).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should handle missing authentication gracefully', () => {
|
|
268
|
+
// Clear authentication
|
|
269
|
+
localStorageMock.clear();
|
|
270
|
+
|
|
271
|
+
// Component should still render (may redirect, but shouldn't crash)
|
|
272
|
+
const page = ChatListPage(mockRouter as any);
|
|
273
|
+
expect(page).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('page structure', () => {
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
280
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should render chat-container', () => {
|
|
284
|
+
const page = ChatListPage(mockRouter as any);
|
|
285
|
+
// The chat-list-page contains chat-container as a child
|
|
286
|
+
expect(page.props?.className).toBe('chat-list-page');
|
|
287
|
+
|
|
288
|
+
// Find the chat-container child
|
|
289
|
+
const children = page.children as VNode[];
|
|
290
|
+
const container = children[0];
|
|
291
|
+
expect(container?.props?.className).toBe('chat-container');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should render chat-header', () => {
|
|
295
|
+
const page = ChatListPage(mockRouter as any);
|
|
296
|
+
const html = renderToString(page);
|
|
297
|
+
|
|
298
|
+
expect(html).toContain('chat-header');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should render page title', () => {
|
|
302
|
+
const page = ChatListPage(mockRouter as any);
|
|
303
|
+
const html = renderToString(page);
|
|
304
|
+
|
|
305
|
+
expect(html).toContain('Messages');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should render search input', () => {
|
|
309
|
+
const page = ChatListPage(mockRouter as any);
|
|
310
|
+
const html = renderToString(page);
|
|
311
|
+
|
|
312
|
+
expect(html).toContain('chat-search');
|
|
313
|
+
expect(html).toContain('Search users...');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should render back button', () => {
|
|
317
|
+
const page = ChatListPage(mockRouter as any);
|
|
318
|
+
const html = renderToString(page);
|
|
319
|
+
|
|
320
|
+
expect(html).toContain('Back to Profile');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should have proper CSS classes', () => {
|
|
324
|
+
const page = ChatListPage(mockRouter as any);
|
|
325
|
+
const html = renderToString(page);
|
|
326
|
+
|
|
327
|
+
expect(html).toContain('chat-list-page');
|
|
328
|
+
expect(html).toContain('chat-container');
|
|
329
|
+
expect(html).toContain('chat-header');
|
|
330
|
+
expect(html).toContain('chat-title');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('user loading', () => {
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
337
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should show loading state initially', () => {
|
|
341
|
+
const page = ChatListPage(mockRouter as any);
|
|
342
|
+
const html = renderToString(page);
|
|
343
|
+
|
|
344
|
+
expect(html).toContain('Loading users');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should call fetch with correct headers', async () => {
|
|
348
|
+
ChatListPage(mockRouter as any);
|
|
349
|
+
|
|
350
|
+
// Wait a bit for the async operation
|
|
351
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
352
|
+
|
|
353
|
+
// The fetch should have been called
|
|
354
|
+
expect(mockFetchResponse).toBeDefined();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should filter out current user from list', async () => {
|
|
358
|
+
// Mock fetch response with users
|
|
359
|
+
mockFetchResponse = {
|
|
360
|
+
ok: true,
|
|
361
|
+
json: async () => ({
|
|
362
|
+
users: [
|
|
363
|
+
{ id: '123', name: 'Current User', email: 'current@example.com', bio: 'Current bio', avatar: '' },
|
|
364
|
+
{ id: '456', name: 'Other User', email: 'other@example.com', bio: 'Other bio', avatar: '' }
|
|
365
|
+
]
|
|
366
|
+
})
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const page = ChatListPage(mockRouter as any);
|
|
370
|
+
|
|
371
|
+
// Wait for async operations
|
|
372
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
373
|
+
triggerRAF();
|
|
374
|
+
|
|
375
|
+
const html = renderToString(page);
|
|
376
|
+
|
|
377
|
+
// Should not contain current user
|
|
378
|
+
expect(html).not.toContain('Current User');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('error handling', () => {
|
|
383
|
+
beforeEach(() => {
|
|
384
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
385
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should handle fetch error', async () => {
|
|
389
|
+
// Update the mock to return error
|
|
390
|
+
(global as any).fetch = async () => ({
|
|
391
|
+
ok: false,
|
|
392
|
+
json: async () => ({ users: [] })
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const page = ChatListPage(mockRouter as any);
|
|
396
|
+
|
|
397
|
+
// Wait for async operations
|
|
398
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
399
|
+
triggerRAF();
|
|
400
|
+
|
|
401
|
+
// Should handle the error gracefully
|
|
402
|
+
expect(page).toBeDefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should handle network error', async () => {
|
|
406
|
+
// Update the mock to throw error
|
|
407
|
+
(global as any).fetch = async () => {
|
|
408
|
+
throw new Error('Network error');
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const page = ChatListPage(mockRouter as any);
|
|
412
|
+
|
|
413
|
+
// Wait for async operations
|
|
414
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
415
|
+
|
|
416
|
+
// Should handle the error gracefully
|
|
417
|
+
expect(page).toBeDefined();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('user list rendering', () => {
|
|
422
|
+
beforeEach(() => {
|
|
423
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
424
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should render empty state when no users', async () => {
|
|
428
|
+
(global as any).fetch = async () => ({
|
|
429
|
+
ok: true,
|
|
430
|
+
json: async () => ({ users: [] })
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const page = ChatListPage(mockRouter as any);
|
|
434
|
+
|
|
435
|
+
// Wait for async operations
|
|
436
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
437
|
+
triggerRAF();
|
|
438
|
+
|
|
439
|
+
// Should render without errors
|
|
440
|
+
expect(page).toBeDefined();
|
|
441
|
+
const html = renderToString(page);
|
|
442
|
+
expect(html).toContain('chat-users-list');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should render user cards when users exist', async () => {
|
|
446
|
+
(global as any).fetch = async () => ({
|
|
447
|
+
ok: true,
|
|
448
|
+
json: async () => ({
|
|
449
|
+
users: [
|
|
450
|
+
{ id: '456', name: 'John Doe', email: 'john@example.com', bio: 'John bio', avatar: '' },
|
|
451
|
+
{ id: '789', name: 'Jane Smith', email: 'jane@example.com', bio: 'Jane bio', avatar: '' }
|
|
452
|
+
]
|
|
453
|
+
})
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const page = ChatListPage(mockRouter as any);
|
|
457
|
+
|
|
458
|
+
// Wait for async operations
|
|
459
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
460
|
+
triggerRAF();
|
|
461
|
+
|
|
462
|
+
// Should render without errors
|
|
463
|
+
expect(page).toBeDefined();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should render user avatar with first letter', async () => {
|
|
467
|
+
(global as any).fetch = async () => ({
|
|
468
|
+
ok: true,
|
|
469
|
+
json: async () => ({
|
|
470
|
+
users: [
|
|
471
|
+
{ id: '456', name: 'Alice', email: 'alice@example.com', bio: '', avatar: '' }
|
|
472
|
+
]
|
|
473
|
+
})
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const page = ChatListPage(mockRouter as any);
|
|
477
|
+
|
|
478
|
+
// Wait for async operations
|
|
479
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
480
|
+
triggerRAF();
|
|
481
|
+
|
|
482
|
+
// Should render without errors
|
|
483
|
+
expect(page).toBeDefined();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should render chat button on user cards', async () => {
|
|
487
|
+
(global as any).fetch = async () => ({
|
|
488
|
+
ok: true,
|
|
489
|
+
json: async () => ({
|
|
490
|
+
users: [
|
|
491
|
+
{ id: '456', name: 'Bob', email: 'bob@example.com', bio: '', avatar: '' }
|
|
492
|
+
]
|
|
493
|
+
})
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const page = ChatListPage(mockRouter as any);
|
|
497
|
+
|
|
498
|
+
// Wait for async operations
|
|
499
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
500
|
+
triggerRAF();
|
|
501
|
+
|
|
502
|
+
// Should render without errors
|
|
503
|
+
expect(page).toBeDefined();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe('search functionality', () => {
|
|
508
|
+
beforeEach(() => {
|
|
509
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
510
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should render search input with correct attributes', () => {
|
|
514
|
+
const page = ChatListPage(mockRouter as any);
|
|
515
|
+
const html = renderToString(page);
|
|
516
|
+
|
|
517
|
+
expect(html).toContain('chat-input');
|
|
518
|
+
expect(html).toContain('type="text"');
|
|
519
|
+
expect(html).toContain('Search users...');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should filter users by name', async () => {
|
|
523
|
+
mockFetchResponse = {
|
|
524
|
+
ok: true,
|
|
525
|
+
json: async () => ({
|
|
526
|
+
users: [
|
|
527
|
+
{ id: '456', name: 'Alice Johnson', email: 'alice@example.com', bio: '', avatar: '' },
|
|
528
|
+
{ id: '789', name: 'Bob Smith', email: 'bob@example.com', bio: '', avatar: '' }
|
|
529
|
+
]
|
|
530
|
+
})
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const page = ChatListPage(mockRouter as any);
|
|
534
|
+
|
|
535
|
+
// Wait for async operations
|
|
536
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
537
|
+
triggerRAF();
|
|
538
|
+
|
|
539
|
+
// Note: Testing search functionality requires DOM interaction
|
|
540
|
+
// For unit tests, we verify the structure exists
|
|
541
|
+
const html = renderToString(page);
|
|
542
|
+
expect(html).toContain('chat-search');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should show no results message when search matches nothing', async () => {
|
|
546
|
+
mockFetchResponse = {
|
|
547
|
+
ok: true,
|
|
548
|
+
json: async () => ({
|
|
549
|
+
users: [
|
|
550
|
+
{ id: '456', name: 'Alice', email: 'alice@example.com', bio: '', avatar: '' }
|
|
551
|
+
]
|
|
552
|
+
})
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const page = ChatListPage(mockRouter as any);
|
|
556
|
+
|
|
557
|
+
// Wait for async operations
|
|
558
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
559
|
+
triggerRAF();
|
|
560
|
+
|
|
561
|
+
// Verify search input exists (actual filtering requires DOM interaction)
|
|
562
|
+
const html = renderToString(page);
|
|
563
|
+
expect(html).toContain('chat-search');
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('navigation', () => {
|
|
568
|
+
beforeEach(() => {
|
|
569
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
570
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should have back button that navigates to profile', () => {
|
|
574
|
+
const page = ChatListPage(mockRouter as any);
|
|
575
|
+
const html = renderToString(page);
|
|
576
|
+
|
|
577
|
+
expect(html).toContain('Back to Profile');
|
|
578
|
+
expect(html).toContain('btn-secondary');
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('component consistency', () => {
|
|
583
|
+
beforeEach(() => {
|
|
584
|
+
localStorageMock.setItem('token', 'fake-token');
|
|
585
|
+
localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should always return the same structure', () => {
|
|
589
|
+
const page1 = ChatListPage(mockRouter as any);
|
|
590
|
+
const page2 = ChatListPage(mockRouter as any);
|
|
591
|
+
|
|
592
|
+
expect(page1.tagName).toBe(page2.tagName);
|
|
593
|
+
expect(page1.props?.className).toBe(page2.props?.className);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should render without errors', () => {
|
|
597
|
+
expect(() => {
|
|
598
|
+
const page = ChatListPage(mockRouter as any);
|
|
599
|
+
renderToString(page);
|
|
600
|
+
}).not.toThrow();
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
});
|