lite_webui / tests /input-bar.test.js
blyon1995's picture
init this repo
ca51841
/**
* Tests for InputBar component behaviour:
* - Send button visual state (rainbow idle / muted sending) and icon markup
* - Enter key sends, Shift+Enter does not
* - /reset and /clean are passed through as text (command handling lives in App)
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { InputBar } from '../src/components/input-bar.js';
import { store } from '../src/store.js';
function makeBar() {
const bar = new InputBar();
document.body.appendChild(bar.render());
return bar;
}
afterEach(() => {
document.body.innerHTML = '';
});
function enableAudio(bar) {
store.saveModelCapabilities({
'audio-model': { text: true, image: false, audio: true },
});
bar.setModel('audio-model');
}
// โ”€โ”€โ”€ Send button appearance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
describe('InputBar โ€“ send button appearance', () => {
it('send button uses the theme hover shell by default', () => {
const bar = makeBar();
const btn = bar.el.querySelector('#send-btn');
expect(btn.className).toContain('hover:bg-[var(--c-hi)]');
expect(btn.className).not.toContain('bg-blue-500');
});
it('send button uses a transparent paper airplane svg with fold lines and gradient strokes', () => {
const bar = makeBar();
const btn = bar.el.querySelector('#send-btn');
expect(btn.querySelector('svg')?.getAttribute('fill')).toBe('none');
expect(btn.querySelectorAll('path')).toHaveLength(3);
expect(btn.querySelector('linearGradient')).not.toBeNull();
});
it('setSending(true) disables the button', () => {
const bar = makeBar();
bar.setSending(true);
expect(bar.el.querySelector('#send-btn').disabled).toBe(true);
});
it('setSending(true) switches to the muted theme state', () => {
const bar = makeBar();
bar.setSending(true);
const btn = bar.el.querySelector('#send-btn');
expect(btn.className).toContain('text-[var(--c-tx3)]');
expect(btn.className).toContain('cursor-not-allowed');
expect(btn.querySelector('linearGradient')).toBeNull();
});
it('setSending(false) re-enables button and restores the idle theme state', () => {
const bar = makeBar();
bar.setSending(true);
bar.setSending(false);
const btn = bar.el.querySelector('#send-btn');
expect(btn.disabled).toBe(false);
expect(btn.className).toContain('hover:bg-[var(--c-hi)]');
expect(btn.querySelector('linearGradient')).not.toBeNull();
});
});
// โ”€โ”€โ”€ Keyboard shortcuts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
describe('InputBar โ€“ keyboard shortcuts', () => {
it('Enter dispatches inputbar:send with the textarea text', () => {
const bar = makeBar();
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
const textarea = bar.el.querySelector('#message-input');
textarea.value = 'hello world';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(events).toHaveLength(1);
expect(events[0].text).toBe('hello world');
document.removeEventListener('inputbar:send', handler);
});
it('Enter clears the textarea after sending', () => {
const bar = makeBar();
const textarea = bar.el.querySelector('#message-input');
textarea.value = 'test message';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(textarea.value).toBe('');
});
it('Shift+Enter does NOT dispatch inputbar:send', () => {
const bar = makeBar();
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
const textarea = bar.el.querySelector('#message-input');
textarea.value = 'hello world';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true }));
expect(events).toHaveLength(0);
document.removeEventListener('inputbar:send', handler);
});
it('Enter does nothing when textarea is blank', () => {
const bar = makeBar();
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
const textarea = bar.el.querySelector('#message-input');
textarea.value = ' ';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(events).toHaveLength(0);
document.removeEventListener('inputbar:send', handler);
});
it('Enter does NOT send while _sending is true', () => {
const bar = makeBar();
bar.setSending(true);
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
const textarea = bar.el.querySelector('#message-input');
textarea.value = 'hello';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(events).toHaveLength(0);
document.removeEventListener('inputbar:send', handler);
});
});
// โ”€โ”€โ”€ Command passthrough โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
describe('InputBar โ€“ command passthrough', () => {
it('dispatches inputbar:send with text="/reset" (command parsed by App, not InputBar)', () => {
const bar = makeBar();
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
const textarea = bar.el.querySelector('#message-input');
textarea.value = '/reset';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(events).toHaveLength(1);
expect(events[0].text).toBe('/reset');
document.removeEventListener('inputbar:send', handler);
});
it('dispatches inputbar:send with text="/clean" (command parsed by App, not InputBar)', () => {
const bar = makeBar();
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
const textarea = bar.el.querySelector('#message-input');
textarea.value = '/clean';
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(events).toHaveLength(1);
expect(events[0].text).toBe('/clean');
document.removeEventListener('inputbar:send', handler);
});
});
describe('InputBar โ€“ audio attachments', () => {
it('dispatches audio upload with default transcribe mode', () => {
const bar = makeBar();
enableAudio(bar);
bar._handleAudioFile(new File(['audio'], 'meeting.wav', { type: 'audio/wav' }));
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
bar.el.querySelector('#send-btn').click();
expect(events).toHaveLength(1);
expect(events[0].audio.file.name).toBe('meeting.wav');
document.removeEventListener('inputbar:send', handler);
});
it('supports pasting audio from the clipboard', async () => {
const bar = makeBar();
enableAudio(bar);
const file = new File(['audio'], 'speech.m4a', { type: 'audio/mp4' });
const preventDefault = vi.fn();
await bar._handlePaste({
preventDefault,
clipboardData: {
items: [
{
type: 'audio/mp4',
getAsFile: () => file,
},
],
},
});
expect(preventDefault).toHaveBeenCalled();
expect(bar._pendingAudio.file.name).toBe('speech.m4a');
});
it('sends audio together with a text instruction', () => {
const bar = makeBar();
enableAudio(bar);
bar._handleAudioFile(new File(['audio'], 'call.ogg', { type: 'audio/ogg' }));
bar.el.querySelector('#message-input').value = 'ๆ€ป็ป“่ฟ™ๆฎตๅฝ•้Ÿณๅ†…ๅฎน';
const events = [];
const handler = (e) => events.push(e.detail);
document.addEventListener('inputbar:send', handler);
bar.el.querySelector('#send-btn').click();
expect(events).toHaveLength(1);
expect(events[0].text).toBe('ๆ€ป็ป“่ฟ™ๆฎตๅฝ•้Ÿณๅ†…ๅฎน');
expect(events[0].audio.file.name).toBe('call.ogg');
document.removeEventListener('inputbar:send', handler);
});
});
// โ”€โ”€โ”€ Context badge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
describe('InputBar โ€“ context display', () => {
it('initial badge shows ctx 0', () => {
const bar = makeBar();
expect(bar.el.querySelector('#context-info').textContent).toBe('ctx 0');
});
it('setContextInfo updates the text with compact token counts', () => {
const bar = makeBar();
bar.setContextInfo(1536, 4096, 3276);
expect(bar.el.querySelector('#context-info').textContent).toBe('ctx 1.5k/4.1k');
});
it('setContextInfo(0, max) shows ctx 0/max', () => {
const bar = makeBar();
bar.setContextInfo(0, 4096, 3276);
expect(bar.el.querySelector('#context-info').textContent).toBe('ctx 0/4.1k');
});
it('setContextInfo below warning threshold uses muted color', () => {
const bar = makeBar();
bar.setContextInfo(2000, 4096, 3276);
expect(bar.el.querySelector('#context-info').className).not.toContain('text-amber-400');
});
it('setContextInfo at warning threshold applies amber warning class', () => {
const bar = makeBar();
bar.setContextInfo(3276, 4096, 3276);
expect(bar.el.querySelector('#context-info').className).toContain('text-amber-400');
});
it('setContextInfo above warning threshold applies amber warning class', () => {
const bar = makeBar();
bar.setContextInfo(3800, 4096, 3276);
expect(bar.el.querySelector('#context-info').className).toContain('text-amber-400');
});
it('setContextInfo back below threshold removes amber class', () => {
const bar = makeBar();
bar.setContextInfo(3800, 4096, 3276);
bar.setContextInfo(500, 4096, 3276);
expect(bar.el.querySelector('#context-info').className).not.toContain('text-amber-400');
});
});