OmniVoice-Studio / frontend /src /test /EngineCompatibilityMatrix.test.jsx
Lê Phi Nam
Deploy to HF Space
94004a2
Raw
History Blame Contribute Delete
9.07 kB
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();
});
});
});