import { newSpecPage } from '@stencil/core/testing'; import { BcxChatList } from '../bcx-chat-list'; import { ChatStorageService } from '../../../services/chat-storage.service'; // Mock services const mockApiService = { getChatMessages: jest.fn(), }; describe('bcx-chat-list', () => { beforeEach(() => { jest.clearAllMocks(); // Mock ChatStorageService.getChats static method jest.spyOn(ChatStorageService, 'getChats').mockReturnValue([ { chatId: 'chat-1', lastMessage: 'Hello', lastMessageTimestamp: '2023-01-01T10:00:00Z' }, { chatId: 'chat-2', lastMessage: 'World', lastMessageTimestamp: '2023-01-01T09:00:00Z' } ]); }); it('renders chat list', async () => { const page = await newSpecPage({ components: [BcxChatList], html: ``, }); expect(page.root).toEqualHtml(`

All conversations

`); }); it('emits chatSelected event on click', async () => { const page = await newSpecPage({ components: [BcxChatList], html: ``, }); const chatList = page.rootInstance as BcxChatList; // Manually inject the api service if needed chatList.apiService = mockApiService as any; const eventSpy = jest.fn(); page.root.addEventListener('chatSelected', eventSpy); // Find the first chat item button const chatItem = page.root.shadowRoot.querySelector('.bcx-chat-list__item') as HTMLElement; chatItem.click(); await page.waitForChanges(); expect(eventSpy).toHaveBeenCalled(); expect(eventSpy.mock.calls[0][0].detail).toBe('chat-1'); }); it('shows loading state when fetching messages', async () => { const page = await newSpecPage({ components: [BcxChatList], html: ``, }); const chatList = page.rootInstance as BcxChatList; chatList.apiService = mockApiService as any; // Mock api response to hang or return promise mockApiService.getChatMessages.mockReturnValue(new Promise(() => {})); const chatItem = page.root.shadowRoot.querySelector('.bcx-chat-list__item') as HTMLElement; chatItem.click(); await page.waitForChanges(); // It should now render the messages view with loading spinner const spinner = page.root.shadowRoot.querySelector('.bcx-chat-list__spinner'); expect(spinner).not.toBeNull(); }); it('renders messages after loading', async () => { const page = await newSpecPage({ components: [BcxChatList], html: ``, }); const chatList = page.rootInstance as BcxChatList; chatList.apiService = mockApiService as any; mockApiService.getChatMessages.mockResolvedValue({ results: [ { id: 'm1', content: 'Hi', author: 'user', created_at: '2023-01-01T10:00:00Z' }, { id: 'm2', content: 'Hello', author: 'ai', created_at: '2023-01-01T10:01:00Z' } ], has_next: false, current_page: 1 }); const chatItem = page.root.shadowRoot.querySelector('.bcx-chat-list__item') as HTMLElement; chatItem.click(); // Wait for promise resolution await page.waitForChanges(); await new Promise(r => setTimeout(r, 0)); await page.waitForChanges(); const messages = page.root.shadowRoot.querySelectorAll('.bcx-chat-list__message'); expect(messages).toHaveLength(2); expect(messages[0].textContent).toContain('Hi'); expect(messages[1].textContent).toContain('Hello'); }); it('handles empty state', async () => { jest.spyOn(ChatStorageService, 'getChats').mockReturnValue([]); const page = await newSpecPage({ components: [BcxChatList], html: ``, }); const emptyState = page.root.shadowRoot.querySelector('.bcx-chat-list__empty'); expect(emptyState).not.toBeNull(); expect(emptyState.textContent).toContain('No conversations yet'); }); it('loads more messages on scroll', async () => { // Mock IntersectionObserver const observe = jest.fn(); const disconnect = jest.fn(); const mockIntersectionObserver = jest.fn(() => ({ observe, disconnect, unobserve: jest.fn(), takeRecords: jest.fn() })); (global as any).IntersectionObserver = mockIntersectionObserver; Object.defineProperty(window, 'IntersectionObserver', { writable: true, configurable: true, value: mockIntersectionObserver }); const page = await newSpecPage({ components: [BcxChatList], html: ``, }); const chatList = page.rootInstance as BcxChatList; chatList.apiService = mockApiService as any; // Initial load mockApiService.getChatMessages.mockResolvedValue({ results: [{ id: '1', content: 'Msg 1', created_at: '2023' }], has_next: true // Important! }); const chatItem = page.root.shadowRoot.querySelector('.bcx-chat-list__item') as HTMLElement; chatItem.click(); await page.waitForChanges(); expect(chatList.messages).toHaveLength(1); expect(chatList.hasMore).toBe(true); // Wait for setupInfiniteScroll timeout await new Promise(r => setTimeout(r, 150)); expect(observe).toHaveBeenCalled(); // Trigger loadMoreMessages mockApiService.getChatMessages.mockResolvedValue({ results: [{ id: '2', content: 'Msg 2', created_at: '2022' }], has_next: false }); // Simulate intersection callback manually since JSDOM doesn't do it const observerCallback = (window.IntersectionObserver as jest.Mock).mock.calls[0][0]; observerCallback([{ isIntersecting: true }]); await page.waitForChanges(); expect(mockApiService.getChatMessages).toHaveBeenCalledWith('chat-1', 2, 20); expect(chatList.messages).toHaveLength(2); // 1 new + 1 old expect(chatList.messages[0].content).toBe('Msg 2'); // Prepend }); });