Skip to main content
Glama
textToSpeech.test.ts15.4 kB
/** * Unit Tests: Text-to-Speech Service * * Tests for the TextToSpeech class including: * - Service initialization * - Speech generation with caching * - Audio playback * - Language management * - Provider availability * - Error handling */ /** * Unit Tests: Text-to-Speech Service * * Tests for the TextToSpeech class including: * - Service initialization * - Speech generation with caching * - Audio playback * - Language management * - Provider availability * - Error handling */ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"; // Mock global fetch const createMockFetch = () => { return mock((url: string | URL | Request, init?: RequestInit) => { const urlString = typeof url === "string" ? url : url.toString(); // Mock Home Assistant API endpoint if (urlString.includes("/api/") && !urlString.includes("/api/tts_get_url") && !urlString.includes("/api/services")) { return Promise.resolve({ ok: true, status: 200, statusText: "OK", json: () => Promise.resolve({ message: "Home Assistant" }), } as Response); } // Mock TTS generation endpoint if (urlString.includes("/api/tts_get_url")) { return Promise.resolve({ ok: true, status: 200, statusText: "OK", json: () => Promise.resolve({ url: "https://ha.local/api/tts/audio/test_audio.mp3" }), } as Response); } // Mock services endpoint if (urlString.includes("/api/services")) { return Promise.resolve({ ok: true, status: 200, statusText: "OK", json: () => Promise.resolve([ { tts: "exists", services: { google_translate: {}, microsoft_tts: {}, openai_tts: {}, } } ]), } as Response); } // Mock media player service endpoint if (urlString.includes("/api/services/media_player/play_media")) { return Promise.resolve({ ok: true, status: 200, statusText: "OK", json: () => Promise.resolve([]), } as Response); } return Promise.resolve({ ok: false, status: 404, statusText: "Not Found", json: () => Promise.resolve({ error: "Not found" }), } as Response); }); }; describe("TextToSpeech Service", () => { let TextToSpeech: any; let TextToSpeechConfig: any; let TTSFeedback: any; let tts: any; let mockFetch: ReturnType<typeof createMockFetch>; let originalFetch: typeof global.fetch; beforeEach(async () => { // Dynamically import to avoid app.config issues const module = await import("../../src/speech/textToSpeech"); TextToSpeech = module.TextToSpeech; // Save original fetch and install mock originalFetch = global.fetch; mockFetch = createMockFetch(); global.fetch = mockFetch as any; const config = { hassHost: "https://ha.local", hassToken: "test-token", language: "en", provider: "google_translate", cache: true, }; tts = new TextToSpeech(config); }); afterEach(async () => { // Restore original fetch global.fetch = originalFetch; // Cleanup service await tts.shutdown(); }); describe("Initialization", () => { test("should initialize successfully with valid config", async () => { await tts.initialize(); expect(mockFetch).toHaveBeenCalled(); }); test("should throw error on initialization failure", async () => { // Create TTS with failing connection const failingFetch = mock(() => Promise.resolve({ ok: false, status: 401, statusText: "Unauthorized", } as Response)); global.fetch = failingFetch as any; const failingTts = new TextToSpeech({ hassHost: "https://invalid.local", hassToken: "invalid-token", language: "en", }); await expect(failingTts.initialize()).rejects.toThrow(); }); test("should emit initialized event", async () => { const initPromise = new Promise<void>((resolve) => { tts.once("initialized", () => resolve()); }); await tts.initialize(); await initPromise; }); }); describe("Speech Generation", () => { beforeEach(async () => { await tts.initialize(); }); test("should generate speech successfully", async () => { const feedback = { text: "Turn on the lights", language: "en", }; const result = await tts.generateSpeech(feedback); expect(result).toHaveProperty("url"); expect(result).toHaveProperty("mediaContentId"); expect(result).toHaveProperty("mediaContentType"); expect(result.url).toContain("ha.local"); }); test("should use default language if not specified", async () => { const feedback = { text: "Test message", }; const result = await tts.generateSpeech(feedback); expect(result).toHaveProperty("url"); }); test("should use custom provider if specified", async () => { const feedback = { text: "Test message", provider: "microsoft_tts", }; const result = await tts.generateSpeech(feedback); expect(result).toHaveProperty("url"); }); test("should emit speech_generated event", async () => { const eventPromise = new Promise<void>((resolve) => { tts.once("speech_generated", (data) => { expect(data).toHaveProperty("text"); expect(data).toHaveProperty("language"); expect(data).toHaveProperty("url"); resolve(); }); }); await tts.generateSpeech({ text: "Test" }); await eventPromise; }); test("should throw error when not initialized", async () => { const uninitializedTts = new TextToSpeech({ hassHost: "https://ha.local", hassToken: "test-token", language: "en", }); await expect(uninitializedTts.generateSpeech({ text: "Test" })).rejects.toThrow( "TextToSpeech service not initialized" ); }); test("should handle generation errors", async () => { const errorFetch = mock(() => Promise.resolve({ ok: false, status: 500, statusText: "Internal Server Error", } as Response)); global.fetch = errorFetch as any; const feedback = { text: "Test" }; await expect(tts.generateSpeech(feedback)).rejects.toThrow(); }); test("should emit speech_error event on failure", async () => { const errorFetch = mock(() => Promise.resolve({ ok: false, status: 500, statusText: "Internal Server Error", } as Response)); global.fetch = errorFetch as any; const errorPromise = new Promise<void>((resolve) => { tts.once("speech_error", (data) => { expect(data).toHaveProperty("text"); expect(data).toHaveProperty("error"); resolve(); }); }); try { await tts.generateSpeech({ text: "Test" }); } catch { // Expected to fail } await errorPromise; }); }); describe("Caching", () => { beforeEach(async () => { await tts.initialize(); }); test("should cache generated speech", async () => { const feedback = { text: "Cached message", language: "en", }; const result1 = await tts.generateSpeech(feedback); const result2 = await tts.generateSpeech(feedback); expect(result1.url).toBe(result2.url); // Fetch should only be called once for TTS generation (not counting initialization) const ttsCallCount = mockFetch.mock.calls.filter( (call: any[]) => call[0]?.toString().includes("/api/tts_get_url") ).length; expect(ttsCallCount).toBe(1); }); test("should distinguish cache by language", async () => { await tts.generateSpeech({ text: "Test", language: "en" }); await tts.generateSpeech({ text: "Test", language: "de" }); const stats = tts.getCacheStats(); expect(stats.size).toBe(2); }); test("should distinguish cache by provider", async () => { await tts.generateSpeech({ text: "Test", provider: "google_translate" }); await tts.generateSpeech({ text: "Test", provider: "microsoft_tts" }); const stats = tts.getCacheStats(); expect(stats.size).toBe(2); }); test("should clear cache", async () => { await tts.generateSpeech({ text: "Test 1" }); await tts.generateSpeech({ text: "Test 2" }); let stats = tts.getCacheStats(); expect(stats.size).toBeGreaterThan(0); tts.clearCache(); stats = tts.getCacheStats(); expect(stats.size).toBe(0); }); test("should respect cache: false option", async () => { const noCacheTts = new TextToSpeech({ hassHost: "https://ha.local", hassToken: "test-token", language: "en", cache: false, }); await noCacheTts.initialize(); await noCacheTts.generateSpeech({ text: "Test" }); await noCacheTts.generateSpeech({ text: "Test" }); const stats = noCacheTts.getCacheStats(); expect(stats.size).toBe(0); await noCacheTts.shutdown(); }); }); describe("Audio Playback", () => { beforeEach(async () => { await tts.initialize(); }); test("should play audio on media player", async () => { const ttsResponse = await tts.generateSpeech({ text: "Test" }); await tts.playAudio(ttsResponse, "media_player.living_room"); const playMediaCalls = mockFetch.mock.calls.filter( (call: any[]) => call[0]?.toString().includes("play_media") ); expect(playMediaCalls.length).toBeGreaterThan(0); }); test("should use default media player if not specified", async () => { const ttsResponse = await tts.generateSpeech({ text: "Test" }); await tts.playAudio(ttsResponse); expect(mockFetch).toHaveBeenCalled(); }); test("should emit audio_playing event", async () => { const eventPromise = new Promise<void>((resolve) => { tts.once("audio_playing", (data) => { expect(data).toHaveProperty("entityId"); expect(data).toHaveProperty("url"); resolve(); }); }); const ttsResponse = await tts.generateSpeech({ text: "Test" }); await tts.playAudio(ttsResponse, "media_player.bedroom"); await eventPromise; }); test("should handle playback errors", async () => { const errorFetch = mock((url: string | URL | Request) => { const urlString = typeof url === "string" ? url : url.toString(); if (urlString.includes("play_media")) { return Promise.resolve({ ok: false, status: 404, statusText: "Entity Not Found", } as Response); } return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}), } as Response); }); global.fetch = errorFetch as any; const ttsResponse = await tts.generateSpeech({ text: "Test" }); await expect(tts.playAudio(ttsResponse)).rejects.toThrow(); }); test("should emit playback_error event on failure", async () => { const errorFetch = mock((url: string | URL | Request) => { const urlString = typeof url === "string" ? url : url.toString(); if (urlString.includes("play_media")) { return Promise.resolve({ ok: false, status: 404, statusText: "Entity Not Found", } as Response); } return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}), } as Response); }); global.fetch = errorFetch as any; const errorPromise = new Promise<void>((resolve) => { tts.once("playback_error", (data) => { expect(data).toHaveProperty("error"); resolve(); }); }); const ttsResponse = await tts.generateSpeech({ text: "Test" }); try { await tts.playAudio(ttsResponse); } catch { // Expected to fail } await errorPromise; }); }); describe("Speak Method", () => { beforeEach(async () => { await tts.initialize(); }); test("should generate and play audio in one call", async () => { await tts.speak({ text: "Complete test", mediaPlayerId: "media_player.kitchen", }); const ttsCallCount = mockFetch.mock.calls.filter( (call: any[]) => call[0]?.toString().includes("/api/tts_get_url") ).length; const playCallCount = mockFetch.mock.calls.filter( (call: any[]) => call[0]?.toString().includes("play_media") ).length; expect(ttsCallCount).toBe(1); expect(playCallCount).toBe(1); }); }); describe("Language Management", () => { test("should set language", () => { tts.setLanguage("de"); expect(tts.getLanguage()).toBe("de"); }); test("should get current language", () => { const language = tts.getLanguage(); expect(language).toBe("en"); }); test("should use new language for speech generation", async () => { await tts.initialize(); tts.setLanguage("es"); await tts.generateSpeech({ text: "Hola" }); expect(mockFetch).toHaveBeenCalled(); }); }); describe("Provider Management", () => { beforeEach(async () => { await tts.initialize(); }); test("should get available TTS providers", async () => { const providers = await tts.getAvailableProviders(); expect(Array.isArray(providers)).toBe(true); expect(providers.length).toBeGreaterThan(0); }); test("should handle provider fetch errors gracefully", async () => { const errorFetch = mock((url: string | URL | Request) => { const urlString = typeof url === "string" ? url : url.toString(); if (urlString.includes("/api/services")) { return Promise.resolve({ ok: false, status: 500, statusText: "Internal Server Error", } as Response); } return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}), } as Response); }); global.fetch = errorFetch as any; const providers = await tts.getAvailableProviders(); // Should return default provider expect(providers).toContain("google_translate"); }); }); describe("Shutdown", () => { test("should clear cache on shutdown", async () => { await tts.initialize(); await tts.generateSpeech({ text: "Test" }); let stats = tts.getCacheStats(); expect(stats.size).toBeGreaterThan(0); await tts.shutdown(); stats = tts.getCacheStats(); expect(stats.size).toBe(0); }); test("should emit shutdown event", async () => { await tts.initialize(); const shutdownPromise = new Promise<void>((resolve) => { tts.once("shutdown", () => resolve()); }); await tts.shutdown(); await shutdownPromise; }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/advanced-homeassistant-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server