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,530 @@
1
+ /**
2
+ * ChatPage Component Unit Tests
3
+ */
4
+
5
+ // CRITICAL: Set up ALL mocks BEFORE importing ChatPage 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 () => ({ messages: [] })
49
+ };
50
+ const mockFetch = async () => mockFetchResponse;
51
+
52
+ // Mock WebSocket - prevent real connection attempts
53
+ class MockWebSocket {
54
+ url: string;
55
+ readyState: number = 3; // Start as closed to prevent connection attempts
56
+ onopen: ((event: Event) => void) | null = null;
57
+ onmessage: ((event: MessageEvent) => void) | null = null;
58
+ onerror: ((event: Event) => void) | null = null;
59
+ onclose: ((event: CloseEvent) => void) | null = null;
60
+
61
+ constructor(url: string) {
62
+ this.url = url;
63
+ // Immediately close to prevent hanging
64
+ if (this.onclose) {
65
+ this.onclose(new CloseEvent('close'));
66
+ }
67
+ }
68
+
69
+ send(data: string) {
70
+ // Do nothing
71
+ return;
72
+ }
73
+
74
+ close() {
75
+ this.readyState = 3;
76
+ }
77
+
78
+ addEventListener(_type: string, _listener: any) {
79
+ // Do nothing
80
+ }
81
+
82
+ removeEventListener(_type: string, _listener: any) {
83
+ // Do nothing
84
+ }
85
+ }
86
+
87
+ // Mock window.addEventListener
88
+ const eventListeners: Record<string, Function[]> = {};
89
+ const addEventListenerMock = (event: string, handler: Function) => {
90
+ if (!eventListeners[event]) {
91
+ eventListeners[event] = [];
92
+ }
93
+ eventListeners[event].push(handler);
94
+ };
95
+
96
+ // Mock requestAnimationFrame
97
+ let rafId = 0;
98
+ const rafCallbacks: Map<number, FrameRequestCallback> = new Map();
99
+ const mockRequestAnimationFrame = (callback: FrameRequestCallback) => {
100
+ const id = ++rafId;
101
+ rafCallbacks.set(id, callback);
102
+ return id;
103
+ };
104
+
105
+ const mockCancelAnimationFrame = (id: number) => {
106
+ rafCallbacks.delete(id);
107
+ };
108
+
109
+ // Mock setInterval/clearInterval - don't actually run intervals in tests
110
+ let intervalId = 0;
111
+ const mockSetInterval = (_callback: () => void, _ms: number) => {
112
+ return ++intervalId;
113
+ };
114
+
115
+ const mockClearInterval = (_id: number) => {
116
+ // Do nothing
117
+ };
118
+
119
+ // Mock location
120
+ const mockLocation = {
121
+ host: 'localhost:3000',
122
+ hostname: 'localhost',
123
+ port: '3000',
124
+ protocol: 'http:',
125
+ href: 'http://localhost:3000/',
126
+ origin: 'http://localhost:3000'
127
+ };
128
+
129
+ // Mock router
130
+ const mockRouter = {
131
+ push: mockFn() as any,
132
+ replace: mockFn() as any,
133
+ go: mockFn() as any,
134
+ back: mockFn() as any,
135
+ forward: mockFn() as any,
136
+ currentPath: '/',
137
+ currentState: null
138
+ };
139
+
140
+ // SETUP GLOBALS BEFORE IMPORT
141
+ // Clear real localStorage if it exists
142
+ if (typeof localStorage !== 'undefined') {
143
+ localStorage.clear();
144
+ }
145
+
146
+ // Set up global mocks
147
+ (global as any).localStorage = localStorageMock;
148
+ (globalThis as any).localStorage = localStorageMock;
149
+ (global as any).fetch = mockFetch;
150
+ (globalThis as any).fetch = mockFetch;
151
+ (global as any).WebSocket = MockWebSocket;
152
+ (globalThis as any).WebSocket = MockWebSocket;
153
+ (global as any).window = {
154
+ addEventListener: addEventListenerMock,
155
+ removeEventListener: mockFn(),
156
+ localStorage: localStorageMock,
157
+ location: mockLocation
158
+ };
159
+ (globalThis as any).window = (global as any).window;
160
+ (global as any).location = mockLocation;
161
+ (globalThis as any).location = mockLocation;
162
+ (global as any).setInterval = mockSetInterval;
163
+ (globalThis as any).setInterval = mockSetInterval;
164
+ (global as any).clearInterval = mockClearInterval;
165
+ (globalThis as any).clearInterval = mockClearInterval;
166
+ (global as any).requestAnimationFrame = mockRequestAnimationFrame;
167
+ (global as any).cancelAnimationFrame = mockCancelAnimationFrame;
168
+ (globalThis as any).requestAnimationFrame = mockRequestAnimationFrame;
169
+ (globalThis as any).cancelAnimationFrame = mockCancelAnimationFrame;
170
+
171
+ // NOW import the component (after mocks are set up)
172
+ import { ChatPage } from './ChatPage';
173
+ import type { VNode } from 'elit/types';
174
+
175
+ // Helper function to render VNode to HTML string
176
+ function renderToString(vNode: VNode | string | number | undefined | null): string {
177
+ if (vNode == null || vNode === false) return '';
178
+ if (typeof vNode !== 'object') return String(vNode);
179
+
180
+ const { tagName, props, children } = vNode;
181
+ const attrs = props ? Object.entries(props)
182
+ .filter(([k, v]) => v != null && v !== false && k !== 'children' && k !== 'ref' && !k.startsWith('on'))
183
+ .map(([k, v]) => {
184
+ if (k === 'className' || k === 'class') return `class="${Array.isArray(v) ? v.join(' ') : v}"`;
185
+ if (k === 'style') return `style="${typeof v === 'string' ? v : Object.entries(v).map(([sk, sv]) => `${sk.replace(/([A-Z])/g, '-$1').toLowerCase()}:${sv}`).join(';')}"`;
186
+ if (v === true) return k;
187
+ return `${k}="${v}"`;
188
+ })
189
+ .join(' ') : '';
190
+
191
+ const childrenStr = children && children.length > 0
192
+ ? children.map(c => renderToString(c as any)).join('')
193
+ : '';
194
+
195
+ return `<${tagName}${attrs ? ' ' + attrs : ''}>${childrenStr}</${tagName}>`;
196
+ }
197
+
198
+ describe('ChatPage Component', () => {
199
+ // Clear REAL browser localStorage before any tests run
200
+ beforeAll(() => {
201
+ if (typeof localStorage !== 'undefined') {
202
+ localStorage.clear();
203
+ }
204
+ });
205
+
206
+ beforeEach(() => {
207
+ // Clear RAF callbacks
208
+ rafCallbacks.clear();
209
+ rafId = 0;
210
+
211
+ // Clear localStorage before each test
212
+ localStorageMock.clear();
213
+
214
+ // Also clear REAL browser localStorage
215
+ if (typeof localStorage !== 'undefined') {
216
+ localStorage.clear();
217
+ }
218
+
219
+ // Reset mock router
220
+ (mockRouter.push as any).mockClear();
221
+ (mockRouter.replace as any).mockClear();
222
+
223
+ // Reset fetch mock
224
+ mockFetchResponse = {
225
+ ok: true,
226
+ json: async () => ({ messages: [] })
227
+ };
228
+
229
+ // Clear event listeners
230
+ Object.keys(eventListeners).forEach(key => {
231
+ delete eventListeners[key];
232
+ });
233
+ });
234
+
235
+ afterEach(() => {
236
+ // Clean up event listeners after each test
237
+ Object.keys(eventListeners).forEach(key => {
238
+ delete eventListeners[key];
239
+ });
240
+
241
+ // Clear RAF callbacks after each test
242
+ rafCallbacks.clear();
243
+ rafId = 0;
244
+
245
+ // Clear REAL browser localStorage after each test
246
+ if (typeof localStorage !== 'undefined') {
247
+ localStorage.clear();
248
+ }
249
+ });
250
+
251
+ describe('authentication', () => {
252
+ it('should render page when authenticated', () => {
253
+ // Set both token and user
254
+ localStorageMock.setItem('token', 'fake-token');
255
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
256
+
257
+ const page = ChatPage(mockRouter as any);
258
+
259
+ expect(page).toBeDefined();
260
+ expect(page.tagName).toBe('div');
261
+ expect(page.props?.className).toBe('chat-page');
262
+ });
263
+
264
+ it('should not redirect when authenticated', () => {
265
+ localStorageMock.setItem('token', 'fake-token');
266
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
267
+
268
+ ChatPage(mockRouter as any);
269
+
270
+ // Should not redirect when authenticated
271
+ expect(mockRouter.push.calls.length).toBe(0);
272
+ });
273
+
274
+ it('should handle missing authentication gracefully', () => {
275
+ // Clear authentication
276
+ localStorageMock.clear();
277
+
278
+ // Component should still render (may redirect, but shouldn't crash)
279
+ const page = ChatPage(mockRouter as any);
280
+ expect(page).toBeDefined();
281
+ });
282
+ });
283
+
284
+ describe('page structure', () => {
285
+ beforeEach(() => {
286
+ localStorageMock.setItem('token', 'fake-token');
287
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
288
+ });
289
+
290
+ it('should render chat-container', () => {
291
+ const page = ChatPage(mockRouter as any);
292
+ expect(page.props?.className).toBe('chat-page');
293
+
294
+ // Find the chat-container child
295
+ const children = page.children as VNode[];
296
+ const container = children[0];
297
+ expect(container?.props?.className).toBe('chat-container');
298
+ });
299
+
300
+ it('should render chat-header', () => {
301
+ const page = ChatPage(mockRouter as any);
302
+ const html = renderToString(page);
303
+
304
+ expect(html).toContain('chat-header');
305
+ });
306
+
307
+ it('should render page title', () => {
308
+ const page = ChatPage(mockRouter as any);
309
+ const html = renderToString(page);
310
+
311
+ expect(html).toContain('Chat Room');
312
+ });
313
+
314
+ it('should render messages area', () => {
315
+ const page = ChatPage(mockRouter as any);
316
+ const html = renderToString(page);
317
+
318
+ expect(html).toContain('chat-messages');
319
+ });
320
+
321
+ it('should render input area', () => {
322
+ const page = ChatPage(mockRouter as any);
323
+ const html = renderToString(page);
324
+
325
+ expect(html).toContain('chat-input-area');
326
+ expect(html).toContain('chat-input');
327
+ });
328
+
329
+ it('should render send button', () => {
330
+ const page = ChatPage(mockRouter as any);
331
+ const html = renderToString(page);
332
+
333
+ expect(html).toContain('Send');
334
+ expect(html).toContain('btn-primary');
335
+ });
336
+
337
+ it('should render back button', () => {
338
+ const page = ChatPage(mockRouter as any);
339
+ const html = renderToString(page);
340
+
341
+ expect(html).toContain('Back to Profile');
342
+ });
343
+
344
+ it('should have proper CSS classes', () => {
345
+ const page = ChatPage(mockRouter as any);
346
+ const html = renderToString(page);
347
+
348
+ expect(html).toContain('chat-page');
349
+ expect(html).toContain('chat-container');
350
+ expect(html).toContain('chat-header');
351
+ expect(html).toContain('chat-title');
352
+ });
353
+ });
354
+
355
+ describe('message loading', () => {
356
+ beforeEach(() => {
357
+ localStorageMock.setItem('token', 'fake-token');
358
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
359
+ });
360
+
361
+ it('should show empty state when no messages', () => {
362
+ (global as any).fetch = async () => ({
363
+ ok: true,
364
+ json: async () => ({ messages: [] })
365
+ });
366
+
367
+ const page = ChatPage(mockRouter as any);
368
+
369
+ // Should render without errors
370
+ expect(page).toBeDefined();
371
+ const html = renderToString(page);
372
+ expect(html).toContain('chat-messages');
373
+ });
374
+
375
+ it('should call fetch on mount', async () => {
376
+ const fetchSpy = mockFn();
377
+ (global as any).fetch = async () => {
378
+ fetchSpy();
379
+ return {
380
+ ok: true,
381
+ json: async () => ({ messages: [] })
382
+ };
383
+ };
384
+
385
+ ChatPage(mockRouter as any);
386
+
387
+ // Wait for async operations
388
+ await new Promise(resolve => setTimeout(resolve, 50));
389
+
390
+ // Fetch should have been called
391
+ expect(fetchSpy.calls.length).toBeGreaterThan(0);
392
+ });
393
+ });
394
+
395
+ describe('input functionality', () => {
396
+ beforeEach(() => {
397
+ localStorageMock.setItem('token', 'fake-token');
398
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
399
+ });
400
+
401
+ it('should render text input with correct attributes', () => {
402
+ const page = ChatPage(mockRouter as any);
403
+ const html = renderToString(page);
404
+
405
+ expect(html).toContain('type="text"');
406
+ expect(html).toContain('chat-input');
407
+ expect(html).toContain('Type your message...');
408
+ });
409
+
410
+ it('should have form element', () => {
411
+ const page = ChatPage(mockRouter as any);
412
+ const html = renderToString(page);
413
+
414
+ expect(html).toContain('<form');
415
+ expect(html).toContain('chat-input-area');
416
+ });
417
+ });
418
+
419
+ describe('error handling', () => {
420
+ beforeEach(() => {
421
+ localStorageMock.setItem('token', 'fake-token');
422
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
423
+ });
424
+
425
+ it('should handle fetch error gracefully', async () => {
426
+ (global as any).fetch = async () => ({
427
+ ok: false,
428
+ json: async () => ({ messages: [] })
429
+ });
430
+
431
+ const page = ChatPage(mockRouter as any);
432
+
433
+ // Wait for async operations
434
+ await new Promise(resolve => setTimeout(resolve, 50));
435
+
436
+ // Should handle the error gracefully
437
+ expect(page).toBeDefined();
438
+ });
439
+
440
+ it('should handle network error gracefully', async () => {
441
+ (global as any).fetch = async () => {
442
+ throw new Error('Network error');
443
+ };
444
+
445
+ const page = ChatPage(mockRouter as any);
446
+
447
+ // Wait for async operations
448
+ await new Promise(resolve => setTimeout(resolve, 50));
449
+
450
+ // Should handle the error gracefully
451
+ expect(page).toBeDefined();
452
+ });
453
+ });
454
+
455
+ describe('navigation', () => {
456
+ beforeEach(() => {
457
+ localStorageMock.setItem('token', 'fake-token');
458
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
459
+ });
460
+
461
+ it('should have back button that navigates to profile', () => {
462
+ const page = ChatPage(mockRouter as any);
463
+ const html = renderToString(page);
464
+
465
+ expect(html).toContain('Back to Profile');
466
+ expect(html).toContain('btn-secondary');
467
+ });
468
+ });
469
+
470
+ describe('user display', () => {
471
+ beforeEach(() => {
472
+ localStorageMock.setItem('token', 'fake-token');
473
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
474
+ });
475
+
476
+ it('should display user name in header', () => {
477
+ const page = ChatPage(mockRouter as any);
478
+ const html = renderToString(page);
479
+
480
+ expect(html).toContain('Logged in as Test User');
481
+ });
482
+
483
+ it('should show logged in user name', () => {
484
+ const page = ChatPage(mockRouter as any);
485
+ const html = renderToString(page);
486
+
487
+ expect(html).toContain('Test User');
488
+ });
489
+ });
490
+
491
+ describe('component consistency', () => {
492
+ beforeEach(() => {
493
+ localStorageMock.setItem('token', 'fake-token');
494
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
495
+ });
496
+
497
+ it('should always return the same structure', () => {
498
+ const page1 = ChatPage(mockRouter as any);
499
+ const page2 = ChatPage(mockRouter as any);
500
+
501
+ expect(page1.tagName).toBe(page2.tagName);
502
+ expect(page1.props?.className).toBe(page2.props?.className);
503
+ });
504
+
505
+ it('should render without errors', () => {
506
+ expect(() => {
507
+ const page = ChatPage(mockRouter as any);
508
+ renderToString(page);
509
+ }).not.toThrow();
510
+ });
511
+ });
512
+
513
+ describe('shared state integration', () => {
514
+ beforeEach(() => {
515
+ localStorageMock.setItem('token', 'fake-token');
516
+ localStorageMock.setItem('user', JSON.stringify({ id: '123', name: 'Test User', email: 'test@example.com', bio: 'Test bio', avatar: '' }));
517
+ });
518
+
519
+ it('should initialize shared state for messages', () => {
520
+ // Mock location to return correct host
521
+ (global as any).location = mockLocation;
522
+ (globalThis as any).location = mockLocation;
523
+
524
+ const page = ChatPage(mockRouter as any);
525
+
526
+ // Should create shared state without crashing
527
+ expect(page).toBeDefined();
528
+ });
529
+ });
530
+ });