create-elit 3.3.3 → 3.3.5

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