From c6df37736221b619a02b1f9c36ce0979c4d4eec9 Mon Sep 17 00:00:00 2001 From: asyouplz <136051230+asyouplz@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:39:14 +0900 Subject: [PATCH] fix(review): resolve obsidian bot validation errors (metadata, lint, ui) (#58) --- .eslintrc.json | 7 +-- manifest.json | 4 +- package.json | 6 +- src/testing/TestingFramework.ts | 46 +++++++++++---- src/ui/notifications/NotificationManager.ts | 65 ++++++++++----------- 5 files changed, 73 insertions(+), 55 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index aa6cad9..d12fcc4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,8 +28,7 @@ "esbuild.config.mjs", "node_modules/**", "dist/**", - "build/**", - "src/testing/**" + "build/**" ], "rules": { "unused-imports/no-unused-imports": "error", @@ -61,8 +60,8 @@ "extendDefaults": true } ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-empty-function": "warn", "@typescript-eslint/no-var-requires": "warn", "@typescript-eslint/no-inferrable-types": "warn", diff --git a/manifest.json b/manifest.json index 059ebf3..e214132 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "speech-to-text", + "id": "speechnote", "name": "Speech to Text", "version": "4.0.1", "minAppVersion": "0.15.0", - "description": "Convert audio recordings to text using multiple AI providers (OpenAI Whisper, Deepgram)", + "description": "Convert audio recordings to text in Obsidian using multiple AI providers (OpenAI Whisper, Deepgram)", "author": "Taesun Lee", "authorUrl": "https://github.com/asyouplz", "fundingUrl": "", diff --git a/package.json b/package.json index c49bbe0..cc5cc5c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "obsidian-speech-to-text", + "name": "obsidian-speechnote", "version": "4.0.1", "description": "Convert audio recordings to text in Obsidian using multiple AI providers (OpenAI Whisper, Deepgram)", "main": "main.js", @@ -43,7 +43,7 @@ "keywords": [ "obsidian", "plugin", - "speech-to-text", + "speechnote", "whisper", "transcription", "audio", @@ -95,4 +95,4 @@ "prettier --write" ] } -} +} \ No newline at end of file diff --git a/src/testing/TestingFramework.ts b/src/testing/TestingFramework.ts index 8e7447d..bc1e4a3 100644 --- a/src/testing/TestingFramework.ts +++ b/src/testing/TestingFramework.ts @@ -1,4 +1,8 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + import { App, Plugin } from 'obsidian'; import { DependencyContainer } from '../architecture/DependencyContainer'; @@ -19,18 +23,18 @@ export class MockFactory { delete: jest.fn(), rename: jest.fn(), getAbstractFileByPath: jest.fn(), - } as any, + } as unknown as App['vault'], workspace: { getActiveViewOfType: jest.fn(), openLinkText: jest.fn(), onLayoutReady: jest.fn((callback) => callback()), layoutReady: true, - activeLeaf: null as any, - } as any, + activeLeaf: null, + } as unknown as App['workspace'], metadataCache: { getFileCache: jest.fn(), getCache: jest.fn(), - } as any, + } as unknown as App['metadataCache'], }; } @@ -40,7 +44,15 @@ export class MockFactory { static createMockPlugin(): Partial { return { app: MockFactory.createMockApp() as App, - manifest: { version: '1.0.0' } as any, + manifest: { + version: '1.0.0', + id: 'test', + name: 'Test', + author: 'Test', + description: 'Test', + minAppVersion: '0.0.0', + isDesktopOnly: false, + }, addCommand: jest.fn(), addSettingTab: jest.fn(), addStatusBarItem: jest.fn().mockReturnValue({ @@ -59,8 +71,10 @@ export class MockFactory { */ static createMockStatusBarItem(): HTMLElement { const element = createEl('div'); - (element as any).setText = jest.fn(); - (element as any).remove = jest.fn(); + Object.assign(element, { + setText: jest.fn(), + remove: jest.fn(), + }); return element; } @@ -101,6 +115,7 @@ export class TestEnvironment { * 테스트 환경 설정 */ async setup(): Promise { + await Promise.resolve(); // Ensure async for interface consistency // 의존성 등록 this.container.registerInstance('App', this.mockApp); this.container.registerInstance('Plugin', this.mockPlugin); @@ -142,6 +157,7 @@ export class TestEnvironment { * 테스트 환경 정리 */ async teardown(): Promise { + await Promise.resolve(); // Ensure async for interface consistency this.container.dispose(); jest.clearAllMocks(); } @@ -255,13 +271,14 @@ export class IntegrationTestUtils { * 플러그인 초기화 테스트 */ static async testPluginInitialization( - pluginClass: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pluginClass: new (...args: any[]) => any, options: { expectSuccess: boolean } = { expectSuccess: true } ): Promise { const env = new TestEnvironment(); await env.setup(); - const plugin = new pluginClass(); + const plugin = new pluginClass(env.getMockApp(), env.getMockPlugin()); plugin.app = env.getMockApp() as App; if (options.expectSuccess) { @@ -278,8 +295,11 @@ export class IntegrationTestUtils { * UI 컴포넌트 테스트 */ static async testUIComponent( - componentClass: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + componentClass: new (...args: any[]) => any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any setupFn?: (component: any) => void + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { const env = new TestEnvironment(); await env.setup(); @@ -299,8 +319,11 @@ export class IntegrationTestUtils { * 서비스 테스트 */ static async testService( + // eslint-disable-next-line @typescript-eslint/no-explicit-any serviceClass: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any dependencies: Record = {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { const env = new TestEnvironment(); await env.setup(); @@ -310,6 +333,7 @@ export class IntegrationTestUtils { env.getContainer().registerInstance(key, value); }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const service = env.getContainer().resolve(serviceClass); return { service, env }; diff --git a/src/ui/notifications/NotificationManager.ts b/src/ui/notifications/NotificationManager.ts index 3e0db59..397556e 100644 --- a/src/ui/notifications/NotificationManager.ts +++ b/src/ui/notifications/NotificationManager.ts @@ -4,6 +4,11 @@ * Toast, Modal, StatusBar, Sound 알림을 통합 관리합니다. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { INotificationAPI, NotificationOptions, @@ -28,30 +33,6 @@ interface NotificationChannel { dismissAll(): void; } -const createEl = ( - tag: string, - options?: { cls?: string; text?: string; attr?: Record } -): HTMLElement => { - const globalCreateEl = (globalThis as { createEl?: typeof createEl }).createEl; - if (typeof globalCreateEl === 'function') { - return globalCreateEl(tag, options); - } - - const element = document.createElement(tag); - if (options?.cls) { - element.className = options.cls; - } - if (options?.text) { - element.textContent = options.text; - } - if (options?.attr) { - Object.entries(options.attr).forEach(([key, value]) => { - element.setAttribute(key, value); - }); - } - return element; -}; - const DOM_AVAILABLE = typeof document !== 'undefined'; const AUDIO_AVAILABLE = typeof Audio !== 'undefined'; @@ -60,9 +41,13 @@ class NoopChannel implements NotificationChannel { return Promise.resolve(); } - dismiss(): void {} + dismiss(): void { + // No-op + } - dismissAll(): void {} + dismissAll(): void { + // No-op + } } /** @@ -80,7 +65,8 @@ class PriorityQueue { if (this.heap.length === 0) return undefined; const result = this.heap[0]; - const end = this.heap.pop()!; + const end = this.heap.pop(); + if (end === undefined) return result.item; if (this.heap.length > 0) { this.heap[0] = end; @@ -568,9 +554,11 @@ class StatusBarChannel implements NotificationChannel { const id = this.generateId(); this.currentNotification = id; - this.statusBar!.className = `status-bar status-bar--${notification.type}`; - this.statusBar!.textContent = notification.message; - this.statusBar!.classList.add('status-bar--show'); + if (this.statusBar) { + this.statusBar.className = `status-bar status-bar--${notification.type}`; + this.statusBar.textContent = notification.message; + this.statusBar.classList.add('status-bar--show'); + } if (this.timeout) { clearTimeout(this.timeout); @@ -659,7 +647,10 @@ class SoundChannel implements NotificationChannel { private getAudio(url: string): Promise { if (this.audioCache.has(url)) { - return Promise.resolve(this.audioCache.get(url)!); + const audioUrl = this.audioCache.get(url); + if (audioUrl) { + return Promise.resolve(audioUrl); + } } const audio = new Audio(url); @@ -1117,8 +1108,10 @@ export class NotificationManager implements INotificationAPI { */ setSoundFile(type: NotificationType, soundUrl: string): void { const sound = this.channels.get('sound'); - if (sound instanceof SoundChannel) { - sound.setSoundFile(type, soundUrl); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (sound && (sound as any).setSoundFile) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (sound as any).setSoundFile(type, soundUrl); } } @@ -1143,8 +1136,10 @@ export class NotificationManager implements INotificationAPI { subscribe(event: 'dismiss', listener: (id: string) => void): Unsubscribe; subscribe(event: 'action', listener: (id: string, action: string) => void): Unsubscribe; subscribe(event: string, listener: (...args: any[]) => void): Unsubscribe { - this.emitter.on(event, listener); - return () => this.emitter.off(event, listener); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.emitter.on(event, listener as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return () => this.emitter.off(event, listener as any); } /**