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; create: () => Promise; } 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('getIcon', () => { it('should return the bot icon', () => { expect(view.getIcon()).toBe('bot'); }); }); 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(); }); }); });