const { test, expect } = require('./_guard'); const path = require('node:path'); test('footer chooses dynamic custom TTS ID and settings preserve custom STT preference', async ({ page }) => { const errors = []; page.on('pageerror', error => errors.push(String(error))); let activated = false; let rejectConnection = false; let lastEngineOnly = false; const connectionChanges = []; const settings = { custom_engine_cards: [ { id: 'stable-stt', name: 'Renamed STT', role: 'stt' }, { id: 'stable-tts', name: 'Renamed voice connection', role: 'tts', isCustom: true, url: 'http://engine/v1' }, ], stt_preferred_backend: 'custom:stable-stt', engine_disabled: {}, engine_models: { 'stable-tts': 'keep-model' }, engine_api_keys: { 'stable-tts': '__configured__' }, engine_local_urls: { 'dc-stable-tts': 'http://engine/v1' }, }; const backends = [ { id: 'openai', label: 'Voice Clone API', title: 'Qwen3 TTS VoiceClone', model: 'tts-1', available: true }, { id: 'custom:stable-tts', label: 'Different API label', title: 'Renamed voice connection', model: 'actual-inference-model', available: true }, { id: 'legacy', label: 'Legacy backend', model: 'legacy-model', available: true }, { id: 'custom:offline', label: 'Offline', available: false }, ]; await page.route(url => url.pathname === '/', route => route.fulfill({ contentType: 'text/html', body: '' })); await page.route('**/api/**', route => { const url = new URL(route.request().url()); if (url.pathname.endsWith('/connection')) { if (rejectConnection) return route.fulfill({ status: 400, json: { ok: false, error: 'Denied' } }); const connected = route.request().postDataJSON().connected; connectionChanges.push(connected); if (connected) delete settings.engine_disabled['stable-tts']; else settings.engine_disabled['stable-tts'] = true; return route.fulfill({ json: { ok: true, connected } }); } if (url.pathname === '/api/probe-url') return route.fulfill({ json: { ok: true } }); if (url.pathname.endsWith('/use-as-engine')) { activated = true; return route.fulfill({ json: { ok: true } }); } if (url.pathname === '/api/tts-backends') { const visible = activated ? backends.filter(backend => backend.id !== 'custom:stable-tts' || !settings.engine_disabled['stable-tts']) : [backends[0]]; return route.fulfill({ json: { backends: lastEngineOnly ? visible.filter(backend => backend.id === 'custom:stable-tts') : visible } }); } return route.fulfill({ json: settings }); }); await page.goto('/'); await page.setContent('
'); await page.evaluate(() => { window._sttBackends = []; window.logErr = () => {}; window.initLlmSnippets = () => {}; }); await page.addScriptTag({ path: path.resolve('static/js/utils.js') }); await page.addScriptTag({ path: path.resolve('static/js/settings.js') }); await page.addScriptTag({ path: path.resolve('static/js/engines.js') }); await page.addScriptTag({ path: path.resolve('static/js/ai-backends.js') }); await page.evaluate(settings => { window._appSettings = settings; window._ttsBackends = [{ id: 'openai', label: 'Default', available: true }]; renderLocalContainers([settings.custom_engine_cards[1]]); }, settings); await page.locator('.dc-use-btn').click(); await expect(page.locator('#tts-backend-select')).toHaveValue('custom:stable-tts'); await expect(page.locator('#tts-backend-select option[value="openai"]')).toHaveText('Qwen3 TTS VoiceClone'); await expect(page.locator('#tts-backend-select option[value="custom:stable-tts"]')).toHaveText('Renamed voice connection'); await expect(page.locator('#tts-backend-select option[value="legacy"]')).toHaveText('Legacy backend'); expect(await page.evaluate(() => { const select = document.createElement('select'); select.innerHTML = styleBackendOptions('custom:stable-tts'); return [...select.options].map(option => ({ id: option.value, label: option.textContent, selected: option.selected })); })).toEqual([ { id: 'openai', label: 'Qwen3 TTS VoiceClone', selected: false }, { id: 'custom:stable-tts', label: 'Renamed voice connection', selected: true }, { id: 'legacy', label: 'Legacy backend', selected: false }, ]); await page.locator('.status-engine').click(); expect(errors).toEqual([]); await expect(page.locator('.status-flyup-item[data-val="custom:stable-tts"]')).toHaveText('Renamed voice connection'); await expect(page.locator('.status-flyup-item[data-val="openai"]')).toHaveText('Qwen3 TTS VoiceClone'); await expect(page.locator('.status-flyup-item[data-val="legacy"]')).toHaveText('Legacy backend'); await expect(page.locator('.status-flyup-item[data-val="custom:offline"]')).toHaveCount(0); await page.locator('.status-flyup-item[data-val="custom:stable-tts"]').click(); await expect(page.locator('#tts-backend-select')).toHaveValue('custom:stable-tts'); await page.locator('#tts-backend-select').selectOption('openai'); await page.locator('.status-engine').click(); await expect(page.locator('.status-flyup-item[data-val="openai"]')).toHaveClass(/is-active/); await page.locator('.status-flyup-item[data-val="custom:stable-tts"]').click(); await expect(page.locator('#tts-backend-select')).toHaveValue('custom:stable-tts'); await page.evaluate(() => { syncPreferredSttBackend(window._appSettings); syncPreferredSttBackend(window._appSettings); }); await expect(page.locator('#s-stt-preferred-backend')).toHaveValue('custom:stable-stt'); await expect(page.locator('#s-stt-preferred-backend option[value="custom:stable-stt"]')).toHaveCount(1); await expect(page.locator('.dc-disconnect-btn')).toBeVisible(); rejectConnection = true; await page.locator('.dc-disconnect-btn').click(); await expect(page.locator('.dc-disconnect-btn')).toBeVisible(); await expect(page.locator('#tts-backend-select')).toHaveValue('custom:stable-tts'); rejectConnection = false; await page.locator('.status-engine').click(); await page.locator('.dc-disconnect-btn').click(); await expect(page.locator('.status-flyup')).toHaveCount(0); await expect(page.locator('#tts-backend-select option[value="custom:stable-tts"]')).toHaveCount(0); await expect(page.locator('.dc-disconnect-btn')).toBeHidden(); await expect(page.locator('.dc-url-inp')).toHaveValue('http://engine/v1'); await expect(page.locator('.dc-model-inp')).toHaveValue('keep-model'); expect(await page.evaluate(() => window._appSettings.engine_api_keys['stable-tts'] === '__configured__')).toBe(true); await page.evaluate(() => { localStorage.setItem('dc-con-stable-tts', '1'); renderLocalContainers([window._appSettings.custom_engine_cards[1]]); }); await expect(page.locator('.dc-disconnect-btn')).toBeHidden(); await expect(page.locator('.dc-connect-btn')).not.toHaveClass(/\bok\b/); await page.locator('.dc-connect-btn').click(); await expect(page.locator('.dc-disconnect-btn')).toBeVisible(); await expect(page.locator('#tts-backend-select option[value="custom:stable-tts"]')).toHaveCount(1); await expect(page.locator('#tts-backend-select')).toHaveValue('openai'); expect(connectionChanges).toEqual([false, true]); lastEngineOnly = true; await page.evaluate(() => refreshTtsBackendAvailability('custom:stable-tts')); await page.locator('.dc-disconnect-btn').click(); await expect(page.locator('#tts-backend-select')).toBeDisabled(); await expect(page.locator('#tts-backend-select')).toHaveValue(''); }); test('LLM footer selection updates active, rehearser and conversation model pickers', async ({ page }) => { const writes = []; await page.route(url => url.pathname === '/', route => route.fulfill({ contentType: 'text/html', body: '' })); await page.route('**/api/**', route => { if (route.request().method() === 'POST') writes.push(route.request().postDataJSON()); return route.fulfill({ json: { ok: true, models: ['old-model', 'picked-model'] } }); }); await page.goto('/'); const options = ''; await page.setContent('' + ['llm-active-model', 'reh-llm-model', 'conv-llm-model-select'].map(id => ``).join('')); await page.evaluate(() => { window._appSettings = { llm_url: 'http://engine/v1', llm_model: 'old-model' }; window.logErr = () => {}; window.applyAndSaveSettings = patch => fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) }); }); await page.addScriptTag({ path: path.resolve('static/js/utils.js') }); await page.addScriptTag({ path: path.resolve('static/js/benchmark-voice-picker.js') }); await page.addScriptTag({ path: path.resolve('static/js/ai-backends.js') }); await expect(page.locator('#llm-active-status')).toHaveText('2 models'); await page.locator('.status-engine').click(); await page.locator('.status-flyup-item[data-val="picked-model"]').click(); for (const id of ['llm-active-model', 'reh-llm-model', 'conv-llm-model-select']) { await expect(page.locator('#' + id)).toHaveValue('picked-model'); } await expect(page.locator('#llm-active-status')).toHaveText('Active model: picked-model'); await expect(page.locator('.bvp-root[data-bvp-for="llm-active-model"] .bvp-trigger-content')).toHaveText('picked-model'); expect(await page.evaluate(() => window._appSettings.llm_model)).toBe('picked-model'); await expect.poll(() => writes.some(patch => patch.llm_model === 'picked-model')).toBe(true); await page.evaluate(() => { window._appSettings.engine_active = { llm_url: 'chosen' }; window._appSettings.engine_disabled = { chosen: true }; document.body.insertAdjacentHTML('beforeend', '
'); window.updateStatusBar(); }); await expect(page.locator('[data-status-kind="llm"] .status-engine-value')).toHaveText('Disconnected'); await page.locator('.status-engine[data-status-kind="llm"]').first().click(); await expect(page.locator('.status-flyup')).toContainText('LLM disconnected'); await expect(page.locator('.status-flyup-item')).toHaveCount(0); expect(await page.evaluate(() => window._appSettings.llm_model)).toBe('picked-model'); }); test('disconnect refresh keeps sidebar navigation on Engines and Settings', async ({ page }) => { const fs = require('node:fs'); const index = fs.readFileSync(path.resolve('static/index.html'), 'utf8'); const tabs = index.match(/