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.
@@ -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
+ });