Files
obsidian_ollama/tests/chat-view.test.ts
T

770 lines
29 KiB
TypeScript
Executable File

import { ChatView } from '../src/chat-view';
import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types';
import { ErrorHandler } from '../src/error-handler';
// Mock Obsidian types
interface MockVault {
getMarkdownFiles: () => any[];
read: () => Promise<string>;
create: () => Promise<any>;
}
interface MockWorkspace {
getLeaf: () => any;
revealLeaf: () => void;
}
interface MockApp {
vault: MockVault;
workspace: MockWorkspace;
}
// Mock Obsidian module - ItemView must set this.app from the leaf
jest.mock('obsidian', () => ({
ItemView: jest.fn().mockImplementation(function (this: any, leaf: any) {
this.app = leaf.app;
}),
WorkspaceLeaf: jest.fn(),
Notice: jest.fn(),
}));
const mockSettings: PluginSettings = {
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
vaultSearchLimit: 3,
maxMessageHistory: 50,
lastIndexTime: 0,
cacheConfig: {
enabled: false,
similarityThreshold: 0.9,
collectionName: 'test-cache',
embeddingModel: 'nomic-embed-text',
chromaUrl: 'http://localhost:8000',
},
};
describe('ChatView', () => {
let view: ChatView;
let mockLeaf: any;
let mockApp: MockApp;
beforeEach(() => {
mockApp = {
vault: {
getMarkdownFiles: jest.fn().mockReturnValue([]),
read: jest.fn().mockResolvedValue(''),
create: jest.fn().mockResolvedValue(null),
},
workspace: {
getLeaf: jest.fn(),
revealLeaf: jest.fn(),
},
};
mockLeaf = {
view: null,
setViewState: jest.fn(),
app: mockApp,
};
view = new ChatView(mockLeaf as unknown as any, mockSettings);
// Obsidian's contentEl has a createEl helper that standard DOM lacks
// Unlike standard DOM, Obsidian elements can create nested elements with createEl
const contentDiv = document.createElement('div') as any;
// Create a factory function that captures the parent element
const createElementWithCreateEl = function (parent: any) {
return function (tag: string, options?: { cls?: string }) {
const el = document.createElement(tag);
if (options?.cls) {
el.classList.add(...options.cls.split(' '));
}
parent.appendChild(el);
// Add createEl to the new element so it can create nested elements
(el as any).createEl = createElementWithCreateEl(el);
return el;
};
};
contentDiv.createEl = createElementWithCreateEl(contentDiv);
view.contentEl = contentDiv;
});
describe('getViewType', () => {
it('should return the correct view type', () => {
expect(view.getViewType()).toBe('ollama-chat-view');
});
});
describe('getDisplayText', () => {
it('should return the correct display text', () => {
expect(view.getDisplayText()).toBe('Ollama Chat');
});
});
describe('onOpen', () => {
it('should call render and setup event listeners', async () => {
const renderSpy = jest.spyOn(view, 'render');
const setupSpy = jest.spyOn(view, 'setupEventListeners' as any);
await view.onOpen();
expect(renderSpy).toHaveBeenCalled();
expect(setupSpy).toHaveBeenCalled();
});
});
describe('onClose', () => {
it('should clean up resources and remove event listeners', async () => {
view['lastMessageEl'] = document.createElement('div');
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
const removeSpy = jest.spyOn(view, 'removeEventListeners' as any);
await view.onClose();
expect(view['lastMessageEl']).toBeNull();
expect(view['sendButton']).toBeNull();
expect(view['inputEl']).toBeNull();
expect(removeSpy).toHaveBeenCalled();
});
});
describe('render', () => {
it('should render the chat interface', async () => {
await view.render();
expect(view.contentEl.querySelector('.ollama-chat-container')).not.toBeNull();
expect(view.contentEl.querySelector('.ollama-input-container')).not.toBeNull();
});
it('should not duplicate elements on re-render', async () => {
// First render
await view.render();
const firstRenderCount = view.contentEl.querySelectorAll('.ollama-message').length;
// Second render with no changes
await view.render();
const secondRenderCount = view.contentEl.querySelectorAll('.ollama-message').length;
expect(secondRenderCount).toBe(firstRenderCount);
});
it('should only render non-streaming messages', async () => {
view['messages'] = [
{ id: '1', role: 'user', content: 'test', timestamp: Date.now() },
{
id: '2',
role: 'assistant',
content: 'response',
timestamp: Date.now(),
isStreaming: true,
},
];
await view.render();
const messages = view.contentEl.querySelectorAll('.ollama-message');
expect(messages.length).toBe(1);
});
});
describe('handleUserInput', () => {
it('should handle user input and call ollamaClient', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'test' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
// Verify that messages were added to conversation history
expect((view as any).messages.length).toBeGreaterThan(0);
});
it('should handle empty user input gracefully', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = ' ';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat');
await (view as any).handleUserInput(' ');
expect(chatSpy).not.toHaveBeenCalled();
});
it('should handle streaming responses and update UI', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'First ' };
yield { role: 'assistant', content: 'chunk ' };
yield { role: 'assistant', content: 'of response' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
// Should have processed all chunks
const messages = (view as any).messages;
const lastMessage = messages[messages.length - 1];
expect(lastMessage.isStreaming).toBe(false);
});
it('should handle streaming re-attach when existing streaming element is found', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).lastMessageEl = document.createElement('div');
(view as any).lastMessageEl.setAttribute('data-msg-id', 'streaming-123');
// Mock querySelector to return existing element
const container = document.createElement('div');
const existingEl = document.createElement('div');
existingEl.setAttribute('data-msg-id', 'streaming-123');
container.appendChild(existingEl);
// Mock the contentEl to return our container
const originalContentEl = (view as any).contentEl;
(view as any).contentEl = container;
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(container.contains((view as any).lastMessageEl)).toBe(true);
// Restore original contentEl
(view as any).contentEl = originalContentEl;
});
it('should append streaming element to container when no existing element found', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
// Mock querySelector to return null (no existing element)
const container = document.createElement('div');
const originalQuerySelector = container.querySelector;
container.querySelector = jest.fn().mockReturnValue(null);
// Mock the contentEl to return our container
const originalContentEl = (view as any).contentEl;
(view as any).contentEl = container;
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(container.childElementCount).toBeGreaterThan(0);
// Restore original contentEl and querySelector
(view as any).contentEl = originalContentEl;
container.querySelector = originalQuerySelector;
});
it('should limit conversation history to maxMessageHistory', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
// Mock a stream that returns quickly
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
// Add enough messages to exceed maxMessageHistory
for (let i = 0; i < 60; i++) {
(view as any).messages.push({
id: `msg-${i}`,
role: 'user',
content: `message ${i}`,
timestamp: Date.now(),
});
}
await (view as any).handleUserInput('test');
// Should be limited to maxMessageHistory
expect((view as any).messages.length).toBeLessThanOrEqual(50);
});
it('should call vaultIndexer.searchVault with user input', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'search query';
const searchSpy = jest.spyOn(view['vaultIndexer'], 'searchVault').mockResolvedValue([]);
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
await (view as any).handleUserInput('search query');
expect(searchSpy).toHaveBeenCalledWith('search query', 3); // Should use DEFAULT_VAULT_SEARCH_LIMIT
expect(chatSpy).toHaveBeenCalled();
});
it('should handle errors during user input gracefully', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
// Mock a stream that throws an error
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
throw new Error('Network error');
})()
);
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
'Ollama Plugin Error [ChatView.handleUserInput]: Network error'
);
});
it('should handle empty user input gracefully', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = ' ';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat');
await (view as any).handleUserInput(' ');
expect(chatSpy).not.toHaveBeenCalled();
});
it('should handle streaming responses and update UI', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'First ' };
yield { role: 'assistant', content: 'chunk ' };
yield { role: 'assistant', content: 'of response' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
// Should have processed all chunks
const messages = (view as any).messages;
const lastMessage = messages[messages.length - 1];
expect(lastMessage.isStreaming).toBe(false);
});
it('should process tool calls with follow-up context', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'test',
tool_calls: [
{
id: 'tool_1',
type: 'function',
function: {
name: 'create_file',
arguments: '{"path":"test/file.md","content":"Test content"}',
},
},
],
};
})()
);
const followUpSpy = jest
.spyOn(view['ollamaClient'], 'chat')
.mockResolvedValue({ role: 'assistant', content: ' follow-up' });
await (view as any).handleUserInput('test');
expect(followUpSpy).toHaveBeenCalled();
// Verify that tool calls resulted in follow-up messages
expect((view as any).messages.length).toBeGreaterThan(1);
});
it('should handle tool call errors gracefully and continue with partial results', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'Response with tools',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'create_file', arguments: '{"path":"test.md","content":"test"}' },
},
{
id: 'call_2',
type: 'function',
function: { name: 'nonexistent_tool', arguments: '{}' },
},
],
};
})()
);
const followUpSpy = jest.spyOn(view['ollamaClient'], 'chat').mockResolvedValue({
role: 'assistant',
content: 'Follow-up response',
tool_calls: [],
});
// Mock tool executor to return mixed results
const toolExecutor = view['toolExecutor'];
jest.spyOn(toolExecutor, 'handleToolCall').mockImplementation(async (call) => {
if (call.function.name === 'create_file') {
return { success: true, message: 'File created successfully' };
} else {
throw new Error('Tool not found');
}
});
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(followUpSpy).toHaveBeenCalled(); // Should still call follow-up with partial results
expect(errorHandlerSpy).toHaveBeenCalledWith(expect.any(Error), 'ChatView.handleUserInput');
expect((view as any).messages.length).toBeGreaterThan(1);
consoleSpy.mockRestore();
errorHandlerSpy.mockRestore();
});
it('should continue processing even when all tool calls fail', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'Response with tools',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'nonexistent_tool_1', arguments: '{}' },
},
{
id: 'call_2',
type: 'function',
function: { name: 'nonexistent_tool_2', arguments: '{}' },
},
],
};
})()
);
const followUpSpy = jest.spyOn(view['ollamaClient'], 'chat').mockResolvedValue({
role: 'assistant',
content: 'Final response despite tool failures',
tool_calls: [],
});
// Mock tool executor to fail for all calls
const toolExecutor = view['toolExecutor'];
jest.spyOn(toolExecutor, 'handleToolCall').mockImplementation(async () => {
throw new Error('Tool not found');
});
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(followUpSpy).not.toHaveBeenCalled(); // Should skip follow-up when no tool results
expect(errorHandlerSpy).toHaveBeenCalledTimes(2); // Should be called for each failed tool call
expect((view as any).messages.length).toBeGreaterThan(1);
errorHandlerSpy.mockRestore();
});
it('should skip follow-up when tool calls result in no successful results', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'Response with tools',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'nonexistent_tool', arguments: '{}' },
},
],
};
})()
);
const followUpSpy = jest.spyOn(view['ollamaClient'], 'chat').mockResolvedValue({
role: 'assistant',
content: 'Final response',
tool_calls: [],
});
// Mock tool executor to fail for the call
const toolExecutor = view['toolExecutor'];
jest.spyOn(toolExecutor, 'handleToolCall').mockImplementation(async () => {
throw new Error('Tool not found');
});
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(followUpSpy).not.toHaveBeenCalled(); // Should skip follow-up when no tool results
expect(errorHandlerSpy).toHaveBeenCalledTimes(1); // Should be called for each failed tool call
errorHandlerSpy.mockRestore();
});
it('should limit conversation history to maxMessageHistory', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
// Mock a stream that returns quickly
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
// Add enough messages to exceed maxMessageHistory
for (let i = 0; i < 60; i++) {
(view as any).messages.push({
id: `msg-${i}`,
role: 'user',
content: `message ${i}`,
timestamp: Date.now(),
});
}
await (view as any).handleUserInput('test');
// Should be limited to maxMessageHistory
expect((view as any).messages.length).toBeLessThanOrEqual(50);
});
it('should call vaultIndexer.searchVault with user input', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'search query';
const searchSpy = jest.spyOn(view['vaultIndexer'], 'searchVault').mockResolvedValue([]);
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
await (view as any).handleUserInput('search query');
expect(searchSpy).toHaveBeenCalledWith('search query', 3); // Should use DEFAULT_VAULT_SEARCH_LIMIT
expect(chatSpy).toHaveBeenCalled();
});
it('should handle errors during user input gracefully', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
// Mock a stream that throws an error
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
throw new Error('Network error');
})()
);
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(errorHandlerSpy).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Network error' }),
'ChatView.handleUserInput'
);
errorHandlerSpy.mockRestore();
});
describe('event handlers', () => {
it('should handle send button click', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).setupEventListeners();
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
const handler = view.getSendButtonClickHandler();
if (!handler) throw new Error('Handler not available');
await handler();
expect(handleSpy).toHaveBeenCalledWith('test');
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
});
it('should handle Enter key press in input', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).setupEventListeners();
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
const event = new KeyboardEvent('keydown', { key: 'Enter' }) as any;
const handler = view.getInputKeyDownHandler();
if (!handler) throw new Error('Handler not available');
await handler(event);
expect(handleSpy).toHaveBeenCalledWith('test');
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
});
it('should not handle Shift+Enter in input', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).setupEventListeners();
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
const event = new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true }) as any;
const handler = view.getInputKeyDownHandler();
if (!handler) throw new Error('Handler not available');
await handler(event);
expect(handleSpy).not.toHaveBeenCalled();
});
it('should handle new chat button click', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
view['newChatButton'] = document.createElement('button');
(view as any).setupEventListeners();
const clearSpy = jest.spyOn(view as any, 'clearConversation');
const handler = view.getNewChatButtonClickHandler();
if (!handler) throw new Error('Handler not available');
handler();
expect(clearSpy).toHaveBeenCalled();
});
});
describe('event listeners', () => {
it('should setup event listeners on open', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
await view.onOpen();
expect(view['sendButtonClickHandler']).not.toBeNull();
expect(view['inputKeyDownHandler']).not.toBeNull();
});
it('should remove event listeners on close', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
await view.onOpen();
const removeSpy = jest.spyOn(view, 'removeEventListeners' as any);
await view.onClose();
expect(removeSpy).toHaveBeenCalled();
});
});
});
describe('event handlers', () => {
it('should handle send button click', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).setupEventListeners();
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
const handler = view.getSendButtonClickHandler();
if (!handler) throw new Error('Handler not available');
await handler();
expect(handleSpy).toHaveBeenCalledWith('test');
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
});
it('should handle Enter key press in input', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).setupEventListeners();
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
const event = new KeyboardEvent('keydown', { key: 'Enter' }) as any;
const handler = view.getInputKeyDownHandler();
if (!handler) throw new Error('Handler not available');
await handler(event);
expect(handleSpy).toHaveBeenCalledWith('test');
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
});
it('should not handle Shift+Enter in input', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).setupEventListeners();
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
const event = new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true }) as any;
const handler = view.getInputKeyDownHandler();
if (!handler) throw new Error('Handler not available');
await handler(event);
expect(handleSpy).not.toHaveBeenCalled();
});
it('should handle new chat button click', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
view['newChatButton'] = document.createElement('button');
(view as any).setupEventListeners();
const clearSpy = jest.spyOn(view as any, 'clearConversation');
const handler = view.getNewChatButtonClickHandler();
if (!handler) throw new Error('Handler not available');
handler();
expect(clearSpy).toHaveBeenCalled();
});
});
describe('event listeners', () => {
it('should setup event listeners on open', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
await view.onOpen();
expect(view['sendButtonClickHandler']).not.toBeNull();
expect(view['inputKeyDownHandler']).not.toBeNull();
});
it('should remove event listeners on close', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
await view.onOpen();
const removeSpy = jest.spyOn(view, 'removeEventListeners' as any);
await view.onClose();
expect(removeSpy).toHaveBeenCalled();
});
});
});