CoolFace
Modelpublic

AXERA-TECH/lite_webui

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes7downloads
app.test.js216 linesDownload Raw Back to tests
1import { afterEach, describe, expect, it, vi } from 'vitest';2import { App } from '../src/components/app.js';3import { store } from '../src/store.js';4 5function setViewport(width) {6  Object.defineProperty(window, 'innerWidth', {7    configurable: true,8    writable: true,9    value: width,10  });11}12 13function mountApp() {14  const root = document.createElement('div');15  document.body.appendChild(root);16  const app = new App(root);17  app._render();18  app._syncSidebarLayout();19  app._loadCurrentConversation();20  app.settingsModal.render();21  app._updateContextInfo();22  return { app, root };23}24 25afterEach(() => {26  document.body.innerHTML = '';27  vi.restoreAllMocks();28});29 30describe('App sidebar layout', () => {31  it('keeps the sidebar visible on desktop when selecting another conversation', () => {32    setViewport(1280);33    const first = store.createConversation('model-a');34    const second = store.createConversation('model-a');35    store.setCurrentConversationId(first.id);36 37    const { app, root } = mountApp();38    const sidebar = root.querySelector('#sidebar');39 40    expect(sidebar.className).not.toContain('-translate-x-full');41 42    app._selectConversation(second.id);43 44    expect(sidebar.className).not.toContain('-translate-x-full');45    expect(sidebar.className).not.toContain('fixed');46    expect(root.querySelector('#new-chat-btn')).not.toBeNull();47  });48 49  it('does not hide the sidebar on desktop after creating multiple new chats', () => {50    setViewport(1280);51    const { app, root } = mountApp();52 53    app._newChat();54    app._newChat();55 56    const sidebar = root.querySelector('#sidebar');57    expect(sidebar.className).not.toContain('-translate-x-full');58    expect(root.querySelectorAll('#conv-list [role="option"]').length).toBe(2);59  });60 61  it('uses off-canvas sidebar behaviour only on mobile widths', () => {62    setViewport(640);63    const first = store.createConversation('model-a');64    const second = store.createConversation('model-a');65    store.setCurrentConversationId(first.id);66 67    const { app, root } = mountApp();68    const sidebar = root.querySelector('#sidebar');69 70    expect(sidebar.className).toContain('-translate-x-full');71    expect(sidebar.className).toContain('fixed');72 73    app._toggleMobileSidebar();74    expect(sidebar.className).toContain('translate-x-0');75    expect(sidebar.className).not.toContain('-translate-x-full');76 77    app._selectConversation(second.id);78    expect(sidebar.className).toContain('-translate-x-full');79  });80});81 82describe('App model state', () => {83  it('uses the globally selected model instead of per-conversation model', () => {84    setViewport(1280);85    store.saveSettings({ baseUrl: 'http://a.local' });86    store.saveAvailableModels('http://a.local', ['model-a']);87    store.setCurrentModel('http://a.local', 'model-a');88 89    const first = store.createConversation('legacy-model');90    store.setCurrentConversationId(first.id);91 92    const { app } = mountApp();93 94    expect(app.modelPicker.getModel()).toBe('model-a');95    expect(app.inputBar._currentModel).toBe('model-a');96  });97 98  it('clears the current model after models:changed when current URL no longer provides it', () => {99    setViewport(1280);100    store.saveSettings({ baseUrl: 'http://a.local' });101    store.saveAvailableModels('http://a.local', ['model-a']);102    store.setCurrentModel('http://a.local', 'model-a');103 104    const { app } = mountApp();105    store.saveAvailableModels('http://a.local', ['model-b']);106    document.dispatchEvent(new CustomEvent('models:changed'));107 108    expect(app.modelPicker.getModel()).toBe('');109    expect(app.inputBar._currentModel).toBe('');110  });111});112 113describe('App audio workflows', () => {114  it('uploads audio and returns transcription text', async () => {115    setViewport(1280);116    store.saveSettings({ baseUrl: 'http://a.local' });117    store.saveAvailableModels('http://a.local', ['audio-model']);118    store.setCurrentModel('http://a.local', 'audio-model');119    store.saveModelCapabilities({ 'audio-model': { text: true, image: false, audio: true } });120 121    globalThis.fetch = vi.fn(async (url) => {122      if (String(url).includes('/v1/audio/transcriptions')) {123        return { ok: true, json: async () => ({ text: '会议录音转写文本' }) };124      }125      throw new Error(`Unexpected fetch: ${url}`);126    });127 128    const { app } = mountApp();129    const file = new File(['audio'], 'meeting.wav', { type: 'audio/wav' });130    await app._handleSend('', null, null, { file, mode: 'transcribe' });131 132    const conv = store.getCurrentConversation();133    expect(conv.messages.at(-1).content).toBe('会议录音转写文本');134  });135 136  it('uploads audio and can return translated text via instruction', async () => {137    setViewport(1280);138    store.saveSettings({ baseUrl: 'http://a.local' });139    store.saveAvailableModels('http://a.local', ['audio-model']);140    store.setCurrentModel('http://a.local', 'audio-model');141    store.saveModelCapabilities({ 'audio-model': { text: true, image: false, audio: true } });142 143    const sseChunks = [144      'data: {"choices":[{"delta":{"content":"Translated meeting notes"}}]}\n\n',145      'data: [DONE]\n\n',146    ];147    let idx = 0;148    const reader = {149      read: vi.fn(async () =>150        idx >= sseChunks.length151          ? { done: true }152          : { done: false, value: new TextEncoder().encode(sseChunks[idx++]) }153      ),154    };155 156    globalThis.fetch = vi.fn(async (url) => {157      if (String(url).includes('/v1/audio/transcriptions')) {158        return { ok: true, json: async () => ({ text: 'Bonjour tout le monde' }) };159      }160      if (String(url).includes('/v1/chat/completions')) {161        return { ok: true, body: { getReader: () => reader } };162      }163      throw new Error(`Unexpected fetch: ${url}`);164    });165 166    const { app } = mountApp();167    const file = new File(['audio'], 'speech.m4a', { type: 'audio/mp4' });168    await app._handleSend('Translate this audio to English', null, null, { file });169 170    const conv = store.getCurrentConversation();171    expect(conv.messages.at(-1).content).toBe('Translated meeting notes');172  });173 174  it('uploads audio with an instruction and returns processed text', async () => {175    setViewport(1280);176    store.saveSettings({ baseUrl: 'http://a.local' });177    store.saveAvailableModels('http://a.local', ['audio-model']);178    store.setCurrentModel('http://a.local', 'audio-model');179    store.saveModelCapabilities({ 'audio-model': { text: true, image: false, audio: true } });180 181    const sseChunks = [182      'data: {"choices":[{"delta":{"content":"待办事项:整理纪要"}}]}\n\n',183      'data: [DONE]\n\n',184    ];185    let idx = 0;186    const reader = {187      read: vi.fn(async () =>188        idx >= sseChunks.length189          ? { done: true }190          : { done: false, value: new TextEncoder().encode(sseChunks[idx++]) }191      ),192    };193 194    let capturedChatBody = '';195    globalThis.fetch = vi.fn(async (url, opts) => {196      if (String(url).includes('/v1/audio/transcriptions')) {197        return { ok: true, json: async () => ({ text: '原始会议录音文本' }) };198      }199      if (String(url).includes('/v1/chat/completions')) {200        capturedChatBody = opts?.body || '';201        return { ok: true, body: { getReader: () => reader } };202      }203      throw new Error(`Unexpected fetch: ${url}`);204    });205 206    const { app } = mountApp();207    const file = new File(['audio'], 'call.wav', { type: 'audio/wav' });208    await app._handleSend('提取会议待办事项', null, null, { file });209 210    const conv = store.getCurrentConversation();211    expect(conv.messages.at(-1).content).toBe('待办事项:整理纪要');212    expect(capturedChatBody).toContain('提取会议待办事项');213    expect(capturedChatBody).toContain('原始会议录音文本');214  });215});216