Increase test coverage across multiple modules (91.2% statements, 82.85% branches, 82.35% functions, 93.1% lines)

Update abort controller handling in OllamaClient to properly clean up previous controllers

Remove unused imports and constants: MODEL_NAME_REGEX, MouseEvent, DEFAULT_SETTINGS, convertMarkdownToHtml

Refactor event handler naming to be more consistent
```
This commit is contained in:
2026-05-06 22:30:29 +02:00
parent 649061a747
commit 579e60f5d5
21 changed files with 2392 additions and 2427 deletions
+38 -51
View File
@@ -4,7 +4,6 @@ import { ItemView, WorkspaceLeaf, Notice } from 'obsidian';
type KeyboardEvent = globalThis.KeyboardEvent;
type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
type HTMLButtonElement = globalThis.HTMLButtonElement;
type MouseEvent = globalThis.MouseEvent;
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
const MAX_MESSAGE_HISTORY = 50;
@@ -29,16 +28,16 @@ export class ChatView extends ItemView {
private vaultIndexer: VaultIndexer;
private toolExecutor: ToolExecutor;
private lastMessageEl: HTMLElement | null = null;
private newChatButton: HTMLElement | null = null;
private sendButton: HTMLElement | null = null;
private inputEl: HTMLElement | null = null;
private newChatButton: HTMLButtonElement | null = null;
private sendButton: HTMLButtonElement | null = null;
private inputEl: HTMLTextAreaElement | null = null;
private chatContainer: HTMLElement | null = null;
private sendButtonClickHandler: (() => Promise<void>) | null = null;
private inputKeyDownHandler: ((e: KeyboardEvent) => Promise<void>) | null = null;
private newChatButtonClickHandler: (() => void) | null = null;
private sendButtonEventHandler: ((e: MouseEvent) => void) | null = null;
private inputKeyDownEventHandler: ((e: KeyboardEvent) => void) | null = null;
private newChatButtonEventHandler: ((e: MouseEvent) => void) | null = null;
private sendButtonClickWrapper: (() => void) | null = null;
private inputKeyDownWrapper: ((e: KeyboardEvent) => void) | null = null;
private newChatButtonClickWrapper: (() => void) | null = null;
private listenersAttached = false;
// Getters for testing
@@ -98,8 +97,9 @@ export class ChatView extends ItemView {
}
private cleanupStreamingResources(): void {
// Ensure any ongoing streaming is properly cleaned up
if (this.lastMessageEl && this.lastMessageEl.parentElement) {
// Only cleanup if there's still an active streaming message
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
if (streamingMessage && this.lastMessageEl && this.lastMessageEl.parentElement) {
this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
this.lastMessageEl = null;
}
@@ -120,7 +120,7 @@ export class ChatView extends ItemView {
this.sendButton = inputContainer.createEl('button', {
cls: 'ollama-send-button',
});
(this.sendButton as HTMLButtonElement).textContent = 'Send';
this.sendButton.textContent = 'Send';
}
if (!this.newChatButton) {
@@ -130,8 +130,8 @@ export class ChatView extends ItemView {
this.newChatButton = newChatContainer.createEl('button', {
cls: 'ollama-new-chat-button',
});
(this.newChatButton as HTMLButtonElement).textContent = '🔄 New Chat';
(this.newChatButton as HTMLButtonElement).title = 'Start a new conversation';
this.newChatButton.textContent = '🔄 New Chat';
this.newChatButton.title = 'Start a new conversation';
}
// Create immutable snapshot for rendering
@@ -183,8 +183,8 @@ export class ChatView extends ItemView {
if (!this.sendButtonClickHandler) {
this.sendButtonClickHandler = async () => {
if (!this.inputEl) return;
await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
(this.inputEl as HTMLTextAreaElement).value = '';
await this.handleUserInput(this.inputEl.value);
this.inputEl.value = '';
};
}
@@ -192,60 +192,47 @@ export class ChatView extends ItemView {
this.inputKeyDownHandler = async (e: KeyboardEvent) => {
if (!this.inputEl || e.key !== 'Enter' || e.shiftKey) return;
e.preventDefault();
await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
(this.inputEl as HTMLTextAreaElement).value = '';
await this.handleUserInput(this.inputEl.value);
this.inputEl.value = '';
};
}
// Add event listeners
this.sendButtonEventHandler = () => {
// Create wrapper functions for event listeners
this.sendButtonClickWrapper = () => {
void this.sendButtonClickHandler?.();
};
this.inputKeyDownEventHandler = (e: KeyboardEvent) => {
this.inputKeyDownWrapper = (e: KeyboardEvent) => {
void this.inputKeyDownHandler?.(e);
};
(this.sendButton as HTMLButtonElement).addEventListener('click', this.sendButtonEventHandler);
(this.inputEl as HTMLTextAreaElement).addEventListener(
'keydown',
this.inputKeyDownEventHandler
);
this.newChatButtonClickWrapper = () => {
void this.newChatButtonClickHandler?.();
};
// Add event listeners using wrappers
this.sendButton.addEventListener('click', this.sendButtonClickWrapper);
this.inputEl.addEventListener('keydown', this.inputKeyDownWrapper);
if (this.newChatButton) {
if (!this.newChatButtonClickHandler) {
this.newChatButtonClickHandler = () => this.clearConversation();
}
this.newChatButtonEventHandler = () => {
this.newChatButtonClickHandler?.();
};
(this.newChatButton as HTMLButtonElement).addEventListener(
'click',
this.newChatButtonEventHandler
);
this.newChatButton.addEventListener('click', this.newChatButtonClickWrapper);
}
this.listenersAttached = true;
}
private removeEventListeners(): void {
if (this.sendButton && this.sendButtonEventHandler) {
(this.sendButton as HTMLButtonElement).removeEventListener(
'click',
this.sendButtonEventHandler
);
if (this.sendButton && this.sendButtonClickWrapper) {
this.sendButton.removeEventListener('click', this.sendButtonClickWrapper);
}
if (this.inputEl && this.inputKeyDownEventHandler) {
(this.inputEl as HTMLTextAreaElement).removeEventListener(
'keydown',
this.inputKeyDownEventHandler
);
if (this.inputEl && this.inputKeyDownWrapper) {
this.inputEl.removeEventListener('keydown', this.inputKeyDownWrapper);
}
if (this.newChatButton && this.newChatButtonEventHandler) {
(this.newChatButton as HTMLButtonElement).removeEventListener(
'click',
this.newChatButtonEventHandler
);
if (this.newChatButton && this.newChatButtonClickWrapper) {
this.newChatButton.removeEventListener('click', this.newChatButtonClickWrapper);
}
this.sendButtonEventHandler = null;
this.inputKeyDownEventHandler = null;
this.newChatButtonEventHandler = null;
this.sendButtonClickWrapper = null;
this.inputKeyDownWrapper = null;
this.newChatButtonClickWrapper = null;
this.listenersAttached = false;
}
@@ -283,7 +270,7 @@ export class ChatView extends ItemView {
private async handleUserInput(content: string) {
if (!this.sendButton || !this.inputEl) return;
(this.sendButton as HTMLButtonElement).disabled = true;
this.sendButton.disabled = true;
try {
// Guard against empty messages
@@ -483,7 +470,7 @@ export class ChatView extends ItemView {
this.cleanupStreamingResources();
} finally {
if (this.sendButton) {
(this.sendButton as HTMLButtonElement).disabled = false;
this.sendButton.disabled = false;
}
}
}