Spaces:
Running
Running
| import React from 'react'; | |
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | |
| import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; | |
| // Mock the toast import the component depends on — keeps the test free | |
| // of side-effect side-channels (toast() schedules timers we don't want). | |
| vi.mock('react-hot-toast', () => ({ | |
| default: { error: vi.fn(), success: vi.fn() }, | |
| toast: { error: vi.fn(), success: vi.fn() }, | |
| })); | |
| import EngineCompatibilityMatrix from '../components/EngineCompatibilityMatrix'; | |
| /** Build a minimal AllEnginesResponse with the three rows the plan calls for. */ | |
| function makeEnginesResponse({ | |
| inProcessAvailable = true, | |
| inProcessHasLastError = false, | |
| } = {}) { | |
| return { | |
| tts: { | |
| active: 'omnivoice', | |
| backends: [ | |
| { | |
| id: 'omnivoice', | |
| display_name: 'OmniVoice (test)', | |
| available: inProcessAvailable, | |
| reason: inProcessAvailable ? null : 'omnivoice package missing', | |
| install_hint: 'pip install omnivoice', | |
| last_error: inProcessHasLastError ? 'previous load failed' : null, | |
| isolation_mode: 'in-process', | |
| gpu_compat: ['cuda', 'mps', 'cpu'], | |
| }, | |
| { | |
| id: 'kittentts', | |
| display_name: 'KittenTTS (test)', | |
| available: false, | |
| reason: 'kittentts not installed', | |
| install_hint: 'pip install kittentts', | |
| last_error: 'auth failed for hf_***REDACTED***', | |
| isolation_mode: 'in-process', | |
| gpu_compat: ['cpu'], | |
| }, | |
| { | |
| id: 'indextts2', | |
| display_name: 'IndexTTS2 (test)', | |
| available: true, | |
| reason: null, | |
| install_hint: 'git clone …', | |
| last_error: null, | |
| isolation_mode: 'subprocess', | |
| gpu_compat: ['cuda', 'mps', 'cpu'], | |
| }, | |
| ], | |
| }, | |
| asr: { active: 'whisperx', backends: [] }, | |
| llm: { active: 'off', backends: [] }, | |
| }; | |
| } | |
| describe('EngineCompatibilityMatrix', () => { | |
| beforeEach(() => { | |
| vi.useRealTimers(); | |
| }); | |
| it('renders one row per backend with the documented columns', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={vi.fn()} | |
| />, | |
| ); | |
| await waitFor(() => { | |
| expect(screen.getByText('OmniVoice (test)')).toBeInTheDocument(); | |
| }); | |
| expect(apiListEngines).toHaveBeenCalledTimes(1); | |
| // Three engine rows, one per registered backend. | |
| expect(screen.getAllByRole('row').length).toBe(3); | |
| expect(screen.getByText('KittenTTS (test)')).toBeInTheDocument(); | |
| expect(screen.getByText('IndexTTS2 (test)')).toBeInTheDocument(); | |
| }); | |
| it('shows isolation_mode badge per row (subprocess for IndexTTS, in-process for the others)', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={vi.fn()} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('IndexTTS2 (test)')); | |
| const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]'); | |
| const omniRow = screen.getByText('OmniVoice (test)').closest('[role="row"]'); | |
| const kittenRow = screen.getByText('KittenTTS (test)').closest('[role="row"]'); | |
| expect(within(indexRow).getByText('subprocess')).toBeInTheDocument(); | |
| expect(within(omniRow).getByText('in-process')).toBeInTheDocument(); | |
| expect(within(kittenRow).getByText('in-process')).toBeInTheDocument(); | |
| }); | |
| it('renders GPU compat chips for each backend', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={vi.fn()} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('OmniVoice (test)')); | |
| const omniRow = screen.getByText('OmniVoice (test)').closest('[role="row"]'); | |
| expect(within(omniRow).getByText('CUDA')).toBeInTheDocument(); | |
| expect(within(omniRow).getByText('MPS')).toBeInTheDocument(); | |
| expect(within(omniRow).getByText('CPU')).toBeInTheDocument(); | |
| const kittenRow = screen.getByText('KittenTTS (test)').closest('[role="row"]'); | |
| // KittenTTS is CPU-only. | |
| expect(within(kittenRow).getByText('CPU')).toBeInTheDocument(); | |
| expect(within(kittenRow).queryByText('CUDA')).not.toBeInTheDocument(); | |
| }); | |
| it('shows the install reason inline when a backend is unavailable', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={vi.fn()} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('KittenTTS (test)')); | |
| const kittenRow = screen.getByText('KittenTTS (test)').closest('[role="row"]'); | |
| expect(within(kittenRow).getByText('kittentts not installed')).toBeInTheDocument(); | |
| expect(within(kittenRow).getByText(/Unavailable/i)).toBeInTheDocument(); | |
| }); | |
| it('renders a "Last error" line when last_error is populated', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={vi.fn()} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('KittenTTS (test)')); | |
| const lastErrEls = screen.getAllByTestId('last-error'); | |
| expect(lastErrEls.length).toBeGreaterThan(0); | |
| // The masked sentinel survives the redactor — confirms the row renders | |
| // the cache verbatim and does NOT try to "clean up" the masked string. | |
| expect(lastErrEls[0].textContent).toMatch(/hf_\*\*\*REDACTED\*\*\*/); | |
| }); | |
| it('clicking Test engine fires getEngineHealth and renders latency_ms', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| const apiGetEngineHealth = vi.fn().mockResolvedValue({ | |
| id: 'indextts2', ok: true, message: 'pong', latency_ms: 1234, | |
| }); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={apiGetEngineHealth} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('IndexTTS2 (test)')); | |
| const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]'); | |
| const testBtn = within(indexRow).getByRole('button', { name: /test indextts2/i }); | |
| fireEvent.click(testBtn); | |
| await waitFor(() => { | |
| expect(apiGetEngineHealth).toHaveBeenCalledWith('indextts2'); | |
| }); | |
| await waitFor(() => { | |
| expect(within(indexRow).getByTestId('health-result-indextts2')).toBeInTheDocument(); | |
| }); | |
| expect(within(indexRow).getByText(/1234 ms/)).toBeInTheDocument(); | |
| }); | |
| it('Test button is disabled while an inflight health request is pending', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| // A health request that never resolves so we can observe the inflight state. | |
| let resolveHealth; | |
| const apiGetEngineHealth = vi.fn(() => new Promise((resolve) => { resolveHealth = resolve; })); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={apiGetEngineHealth} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('IndexTTS2 (test)')); | |
| const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]'); | |
| const testBtn = within(indexRow).getByRole('button', { name: /test indextts2/i }); | |
| fireEvent.click(testBtn); | |
| await waitFor(() => { | |
| expect(testBtn).toBeDisabled(); | |
| }); | |
| // Second click while inflight must be a no-op — the spy has been called | |
| // exactly once. | |
| fireEvent.click(testBtn); | |
| expect(apiGetEngineHealth).toHaveBeenCalledTimes(1); | |
| // Release the promise so the test doesn't leak a pending microtask. | |
| resolveHealth({ id: 'indextts2', ok: true, message: 'pong', latency_ms: 50 }); | |
| }); | |
| it('renders a failure marker when the health route returns ok=false', async () => { | |
| const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); | |
| const apiGetEngineHealth = vi.fn().mockResolvedValue({ | |
| id: 'indextts2', ok: false, message: 'spawn failed', latency_ms: 12, | |
| }); | |
| render( | |
| <EngineCompatibilityMatrix | |
| family="tts" | |
| apiListEngines={apiListEngines} | |
| apiGetEngineHealth={apiGetEngineHealth} | |
| />, | |
| ); | |
| await waitFor(() => screen.getByText('IndexTTS2 (test)')); | |
| const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]'); | |
| fireEvent.click(within(indexRow).getByRole('button', { name: /test indextts2/i })); | |
| await waitFor(() => { | |
| expect(within(indexRow).getByText(/failed/i)).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |