Files
obsidian_ollama/tests/chat-view.test.ts
T
fegger 951c3bbc92 Fix variable naming consistency for chroma URL configuration
Update chromaUrl to chromaURL throughout the codebase to ensure consistent naming convention for the Chroma database URL
configuration parameter. This change affects the semantic cache service implementation and related tests.

The change updates the configuration property name from `chromaUrl` to `chromaURL` in:
- SemanticCacheService class
- Test files (chat-view.test.ts, ollama-client-cache.test.ts, semantic-cache.test.ts)

This maintains consistency with other URL configuration parameters in the codebase and improves code readability.
2026-05-07 22:49:07 +02:00

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