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(`
`);
});
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
});
});