Files
bDS/tests/engine/a2ui/generator.test.ts
2026-02-26 09:27:22 +01:00

264 lines
9.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
isRenderTool,
generateFromToolCall,
generateChart,
generateTable,
generateForm,
generateCard,
generateMetric,
generateList,
generateTabs,
} from '../../../src/main/a2ui/generator';
import type { A2UIServerMessage } from '../../../src/main/a2ui/types';
describe('A2UI generator', () => {
describe('isRenderTool', () => {
it('returns true for all render tools', () => {
expect(isRenderTool('render_chart')).toBe(true);
expect(isRenderTool('render_table')).toBe(true);
expect(isRenderTool('render_form')).toBe(true);
expect(isRenderTool('render_card')).toBe(true);
expect(isRenderTool('render_metric')).toBe(true);
expect(isRenderTool('render_list')).toBe(true);
expect(isRenderTool('render_tabs')).toBe(true);
});
it('returns false for non-render tools', () => {
expect(isRenderTool('search_posts')).toBe(false);
expect(isRenderTool('get_post')).toBe(false);
expect(isRenderTool('render_unknown')).toBe(false);
expect(isRenderTool('')).toBe(false);
});
});
describe('generateFromToolCall', () => {
it('dispatches to chart generator', () => {
const messages = generateFromToolCall('conv-1', 'render_chart', {
chartType: 'bar',
series: [{ label: 'A', value: 1 }],
});
expect(messages).not.toBeNull();
expect(messages!.length).toBeGreaterThanOrEqual(2);
expect(messages![0].type).toBe('createSurface');
});
it('returns null for unknown tool', () => {
expect(generateFromToolCall('conv-1', 'search_posts', {})).toBeNull();
});
});
describe('generateChart', () => {
it('creates surface with chart component and data binding', () => {
const messages = generateChart('conv-1', {
chartType: 'bar',
title: 'Sales',
series: [
{ label: 'Jan', value: 10 },
{ label: 'Feb', value: 20 },
],
});
expect(messages).toHaveLength(3); // createSurface + updateComponents + updateDataModel
const createMsg = messages[0] as Extract<A2UIServerMessage, { type: 'createSurface' }>;
expect(createMsg.type).toBe('createSurface');
expect(createMsg.conversationId).toBe('conv-1');
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
expect(updateMsg.type).toBe('updateComponents');
expect(updateMsg.components).toHaveLength(1);
expect(updateMsg.components[0].type).toBe('chart');
expect(updateMsg.components[0].properties.chartType).toBe('bar');
expect(updateMsg.components[0].dataBinding).toBe('/chartData');
expect(updateMsg.rootIds).toHaveLength(1);
const dataMsg = messages[2] as Extract<A2UIServerMessage, { type: 'updateDataModel' }>;
expect(dataMsg.type).toBe('updateDataModel');
expect(dataMsg.path).toBe('/chartData');
expect(dataMsg.value).toEqual([
{ label: 'Jan', value: 10 },
{ label: 'Feb', value: 20 },
]);
});
});
describe('generateTable', () => {
it('creates surface with table component and row data', () => {
const messages = generateTable('conv-1', {
title: 'Posts',
columns: ['Title', 'Status'],
rows: [['Hello', 'published'], ['Draft', 'draft']],
});
expect(messages).toHaveLength(3);
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
expect(updateMsg.components[0].type).toBe('table');
expect(updateMsg.components[0].properties.columns).toEqual(['Title', 'Status']);
const dataMsg = messages[2] as Extract<A2UIServerMessage, { type: 'updateDataModel' }>;
expect(dataMsg.path).toBe('/tableRows');
expect(dataMsg.value).toEqual([['Hello', 'published'], ['Draft', 'draft']]);
});
});
describe('generateCard', () => {
it('creates surface with card component', () => {
const messages = generateCard('conv-1', {
title: 'My Card',
body: 'Card body text',
subtitle: 'Optional subtitle',
});
expect(messages).toHaveLength(2); // No data model for card
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
expect(updateMsg.components[0].type).toBe('card');
expect(updateMsg.components[0].properties.title).toBe('My Card');
expect(updateMsg.components[0].properties.body).toBe('Card body text');
});
it('includes card actions when provided', () => {
const messages = generateCard('conv-1', {
title: 'Action Card',
body: 'Has actions',
actions: [{ label: 'Open', action: 'openPost', payload: { postId: 'p1' } }],
});
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
expect(updateMsg.components[0].actions).toEqual([
{ eventType: 'click', action: 'openPost', payload: { postId: 'p1' } },
]);
});
});
describe('generateMetric', () => {
it('creates surface with metric component', () => {
const messages = generateMetric('conv-1', {
label: 'Total Posts',
value: '42',
});
expect(messages).toHaveLength(2);
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
expect(updateMsg.components[0].type).toBe('metric');
expect(updateMsg.components[0].properties.label).toBe('Total Posts');
expect(updateMsg.components[0].properties.value).toBe('42');
});
});
describe('generateList', () => {
it('creates surface with list component and item data', () => {
const messages = generateList('conv-1', {
title: 'Tags',
items: ['react', 'typescript', 'electron'],
});
expect(messages).toHaveLength(3);
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
expect(updateMsg.components[0].type).toBe('list');
const dataMsg = messages[2] as Extract<A2UIServerMessage, { type: 'updateDataModel' }>;
expect(dataMsg.path).toBe('/listItems');
expect(dataMsg.value).toEqual(['react', 'typescript', 'electron']);
});
});
describe('generateForm', () => {
it('creates surface with form, field components, and submit button', () => {
const messages = generateForm('conv-1', {
title: 'Edit Post',
submitLabel: 'Save',
fields: [
{ key: 'title', label: 'Title', inputType: 'text', defaultValue: 'Hello' },
{ key: 'draft', label: 'Draft', inputType: 'checkbox', defaultValue: true },
],
});
// createSurface + updateComponents + 2 updateDataModel (one per default value)
expect(messages).toHaveLength(4);
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
// form + 2 fields + 1 submit button = 4
expect(updateMsg.components).toHaveLength(4);
const formComponent = updateMsg.components.find((c) => c.type === 'form');
expect(formComponent).toBeDefined();
expect(formComponent!.children).toHaveLength(3); // 2 fields + submit button
const textField = updateMsg.components.find((c) => c.type === 'textField');
expect(textField).toBeDefined();
expect(textField!.dataBinding).toBe('/formData/title');
const checkBox = updateMsg.components.find((c) => c.type === 'checkBox');
expect(checkBox).toBeDefined();
expect(checkBox!.dataBinding).toBe('/formData/draft');
const submitButton = updateMsg.components.find((c) => c.type === 'button');
expect(submitButton).toBeDefined();
expect(submitButton!.properties.label).toBe('Save');
});
it('maps select inputType to choicePicker', () => {
const messages = generateForm('conv-1', {
submitLabel: 'Go',
fields: [
{
key: 'status',
label: 'Status',
inputType: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
},
],
});
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
const picker = updateMsg.components.find((c) => c.type === 'choicePicker');
expect(picker).toBeDefined();
});
it('maps date inputType to dateTimeInput', () => {
const messages = generateForm('conv-1', {
submitLabel: 'Set',
fields: [{ key: 'date', label: 'Date', inputType: 'date' }],
});
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
const dateInput = updateMsg.components.find((c) => c.type === 'dateTimeInput');
expect(dateInput).toBeDefined();
});
});
describe('generateTabs', () => {
it('creates surface with tabs and child components', () => {
const messages = generateTabs('conv-1', {
tabs: [
{
label: 'Overview',
content: [{ type: 'text', text: 'Tab content' }],
},
{
label: 'Details',
content: [{ type: 'metric', label: 'Count', value: '5' }],
},
],
});
expect(messages).toHaveLength(2); // createSurface + updateComponents
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
const tabsComponent = updateMsg.components.find((c) => c.type === 'tabs');
expect(tabsComponent).toBeDefined();
expect(tabsComponent!.children).toHaveLength(2);
expect(tabsComponent!.properties.tabLabels).toEqual(['Overview', 'Details']);
});
});
});