Skip to main content
Glama
block-builder.test.ts18.3 kB
import { createFormTitleBlock, createQuestionBlocks, buildBlocksForForm } from '../block-builder'; import { QuestionType, QuestionConfig, FormConfig } from '../../models/form-config'; describe('BlockBuilder utility', () => { // RFC 4122 v4 UUID regex pattern for validation const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; describe('createFormTitleBlock', () => { it('returns a FORM_TITLE block with correct payload and metadata', () => { const titleText = 'Sample Form'; const block = createFormTitleBlock(titleText); expect(block.type).toBe('FORM_TITLE'); expect(block.title).toBe(titleText); expect(block.payload).toHaveProperty('html', titleText); expect(block.uuid).toMatch(UUID_V4_REGEX); expect(block.groupUuid).toMatch(UUID_V4_REGEX); // groupType for form titles defaults to TEXT to align with Tally docs expect(block.groupType).toBe('TEXT'); }); it('generates unique UUIDs for multiple calls', () => { const block1 = createFormTitleBlock('Form 1'); const block2 = createFormTitleBlock('Form 2'); expect(block1.uuid).not.toBe(block2.uuid); expect(block1.groupUuid).not.toBe(block2.groupUuid); expect(block1.uuid).toMatch(UUID_V4_REGEX); expect(block2.uuid).toMatch(UUID_V4_REGEX); }); it('handles empty title string', () => { const block = createFormTitleBlock(''); expect(block.title).toBe(''); expect(block.payload.html).toBe(''); expect(block.type).toBe('FORM_TITLE'); expect(block.uuid).toMatch(UUID_V4_REGEX); }); it('handles special characters in title', () => { const specialTitle = 'Test & <HTML> "Quotes" \'Single\' 日本語'; const block = createFormTitleBlock(specialTitle); expect(block.title).toBe(specialTitle); expect(block.payload.html).toBe(specialTitle); }); it('validates payload structure matches Tally API specification', () => { const block = createFormTitleBlock('Test Form'); // Validate required properties exist expect(block).toHaveProperty('uuid'); expect(block).toHaveProperty('type'); expect(block).toHaveProperty('groupUuid'); expect(block).toHaveProperty('groupType'); expect(block).toHaveProperty('title'); expect(block).toHaveProperty('payload'); // Validate payload structure expect(typeof block.payload).toBe('object'); expect(block.payload).toHaveProperty('html'); expect(typeof block.payload.html).toBe('string'); }); }); describe('createQuestionBlocks', () => { const baseTextQuestion: QuestionConfig = { type: QuestionType.TEXT, label: 'First name', required: true, placeholder: 'John', } as any; it('creates TITLE and INPUT_TEXT blocks for a simple text question', () => { const blocks = createQuestionBlocks(baseTextQuestion); expect(blocks).toHaveLength(2); const [titleBlock, inputBlock] = blocks; expect(titleBlock.type).toBe('TITLE'); expect(inputBlock.type).toBe('INPUT_TEXT'); // Ensure both blocks share the same groupUuid expect(titleBlock.groupUuid).toBe(inputBlock.groupUuid); expect(inputBlock.groupType).toBe('INPUT_TEXT'); }); it('validates UUID format for all generated blocks', () => { const blocks = createQuestionBlocks(baseTextQuestion); blocks.forEach(block => { expect(block.uuid).toMatch(UUID_V4_REGEX); expect(block.groupUuid).toMatch(UUID_V4_REGEX); }); }); it('handles required field correctly', () => { const requiredQuestion: QuestionConfig = { type: QuestionType.TEXT, label: 'Required field', required: true, } as any; const optionalQuestion: QuestionConfig = { type: QuestionType.TEXT, label: 'Optional field', required: false, } as any; const requiredBlocks = createQuestionBlocks(requiredQuestion); const optionalBlocks = createQuestionBlocks(optionalQuestion); const requiredInput = requiredBlocks.find(b => b.type === 'INPUT_TEXT'); const optionalInput = optionalBlocks.find(b => b.type === 'INPUT_TEXT'); expect(requiredInput?.payload.isRequired).toBe(true); expect(optionalInput?.payload.isRequired).toBe(false); }); it('handles missing required property gracefully', () => { const questionWithoutRequired: QuestionConfig = { type: QuestionType.TEXT, label: 'Field without required property', } as any; const blocks = createQuestionBlocks(questionWithoutRequired); const inputBlock = blocks.find(b => b.type === 'INPUT_TEXT'); expect(inputBlock?.payload.isRequired).toBe(false); // defaults to false }); it('handles placeholder correctly', () => { const questionWithPlaceholder: QuestionConfig = { type: QuestionType.TEXT, label: 'Field with placeholder', placeholder: 'Enter your name', } as any; const questionWithoutPlaceholder: QuestionConfig = { type: QuestionType.TEXT, label: 'Field without placeholder', } as any; const blocksWithPlaceholder = createQuestionBlocks(questionWithPlaceholder); const blocksWithoutPlaceholder = createQuestionBlocks(questionWithoutPlaceholder); const inputWithPlaceholder = blocksWithPlaceholder.find(b => b.type === 'INPUT_TEXT'); const inputWithoutPlaceholder = blocksWithoutPlaceholder.find(b => b.type === 'INPUT_TEXT'); expect(inputWithPlaceholder?.payload.placeholder).toBe('Enter your name'); expect(inputWithoutPlaceholder?.payload.placeholder).toBe(''); }); it('creates option blocks for dropdown questions', () => { const dropdownQuestion: QuestionConfig = { type: QuestionType.DROPDOWN, label: 'Select color', required: false, options: [ { text: 'Red' }, { text: 'Blue' }, { text: 'Green' }, ], } as any; const blocks = createQuestionBlocks(dropdownQuestion); // Expected: 1 TITLE block + 3 DROPDOWN_OPTION blocks expect(blocks.length).toBe(4); const title = blocks[0]; const optionBlocks = blocks.slice(1); expect(title.type).toBe('TITLE'); optionBlocks.forEach((b) => expect(b.type).toBe('DROPDOWN_OPTION')); // Validate option blocks have correct payload structure optionBlocks.forEach((block, idx) => { expect(block.payload).toHaveProperty('index', idx); expect(block.payload).toHaveProperty('text'); expect(block.title).toBeTruthy(); }); }); it('creates option blocks for multiple choice questions', () => { const multipleChoiceQuestion: QuestionConfig = { type: QuestionType.MULTIPLE_CHOICE, label: 'Choose options', required: false, options: [ { text: 'Option A' }, { text: 'Option B' }, ], } as any; const blocks = createQuestionBlocks(multipleChoiceQuestion); const optionBlocks = blocks.slice(1); optionBlocks.forEach((b) => expect(b.type).toBe('MULTIPLE_CHOICE_OPTION')); }); it('creates option blocks for checkbox questions', () => { const checkboxQuestion: QuestionConfig = { type: QuestionType.CHECKBOXES, label: 'Select all that apply', required: false, options: [ { text: 'Checkbox A' }, { text: 'Checkbox B' }, ], } as any; const blocks = createQuestionBlocks(checkboxQuestion); const optionBlocks = blocks.slice(1); optionBlocks.forEach((b) => expect(b.type).toBe('CHECKBOX')); }); it('handles options with different formats', () => { const questionWithVariedOptions: QuestionConfig = { type: QuestionType.DROPDOWN, label: 'Varied options', options: [ { text: 'Text only' }, { value: 'Value only' }, { id: 'ID only' }, // This becomes [object Object] since BlockBuilder doesn't use id 'String option', ], } as any; const blocks = createQuestionBlocks(questionWithVariedOptions); const optionBlocks = blocks.slice(1); expect(optionBlocks[0].title).toBe('Text only'); expect(optionBlocks[1].title).toBe('Value only'); expect(optionBlocks[2].title).toBe('[object Object]'); // String conversion of { id: 'ID only' } expect(optionBlocks[3].title).toBe('String option'); }); it('handles fields without options correctly', () => { // This tests question types that don't have options const simpleFieldTypes = [ QuestionType.LINEAR_SCALE, QuestionType.RATING, QuestionType.FILE, QuestionType.SIGNATURE, ]; simpleFieldTypes.forEach(type => { const question: QuestionConfig = { type, label: `Test ${type}`, required: false, } as any; const blocks = createQuestionBlocks(question); // Should have TITLE and field type blocks (not option blocks) expect(blocks).toHaveLength(2); expect(blocks[0].type).toBe('TITLE'); expect(blocks[1].type).toBe( type === QuestionType.LINEAR_SCALE ? 'LINEAR_SCALE' : type === QuestionType.RATING ? 'RATING' : type === QuestionType.FILE ? 'FILE_UPLOAD' : 'SIGNATURE' ); // These blocks should not have options in payload since questionHasOptions returns false expect(blocks[1].payload.options).toBeUndefined(); }); }); it('maps each QuestionType to the correct Tally block type', () => { const mappings: Array<[QuestionType, string]> = [ [QuestionType.TEXT, 'INPUT_TEXT'], [QuestionType.EMAIL, 'INPUT_EMAIL'], [QuestionType.NUMBER, 'INPUT_NUMBER'], [QuestionType.PHONE, 'INPUT_PHONE_NUMBER'], [QuestionType.URL, 'INPUT_LINK'], [QuestionType.DATE, 'INPUT_DATE'], [QuestionType.TIME, 'INPUT_TIME'], [QuestionType.TEXTAREA, 'TEXTAREA'], [QuestionType.DROPDOWN, 'DROPDOWN_OPTION'], [QuestionType.CHECKBOXES, 'CHECKBOX'], [QuestionType.MULTIPLE_CHOICE, 'MULTIPLE_CHOICE_OPTION'], [QuestionType.LINEAR_SCALE, 'LINEAR_SCALE'], [QuestionType.RATING, 'RATING'], [QuestionType.FILE, 'FILE_UPLOAD'], [QuestionType.SIGNATURE, 'SIGNATURE'], ]; mappings.forEach(([questionType, expectedBlockType]) => { const question: QuestionConfig = { type: questionType, label: `label-${questionType}`, required: false, ...(questionType === QuestionType.DROPDOWN || questionType === QuestionType.CHECKBOXES || questionType === QuestionType.MULTIPLE_CHOICE ? { options: [{ text: 'Option A' }, { text: 'Option B' }] } : {}), } as any; const blocks = createQuestionBlocks(question); const inputBlock = blocks.find((b) => b.type !== 'TITLE'); expect(inputBlock).toBeDefined(); expect((inputBlock as any).type).toBe(expectedBlockType); }); }); it('validates group metadata consistency', () => { const question: QuestionConfig = { type: QuestionType.TEXT, label: 'Test question', required: true, } as any; const blocks = createQuestionBlocks(question); const [titleBlock, inputBlock] = blocks; // Both blocks should share the same groupUuid expect(titleBlock.groupUuid).toBe(inputBlock.groupUuid); // Title block should have QUESTION groupType expect(titleBlock.groupType).toBe('QUESTION'); // Input block should have groupType matching its type expect(inputBlock.groupType).toBe('INPUT_TEXT'); }); it('validates payload structure for non-option fields', () => { const question: QuestionConfig = { type: QuestionType.EMAIL, label: 'Email field', required: true, placeholder: 'Enter email', } as any; const blocks = createQuestionBlocks(question); const inputBlock = blocks.find(b => b.type === 'INPUT_EMAIL'); expect(inputBlock?.payload).toHaveProperty('isRequired', true); expect(inputBlock?.payload).toHaveProperty('placeholder', 'Enter email'); expect(inputBlock?.payload).not.toHaveProperty('options'); }); it('handles edge case: question with empty label', () => { const question: QuestionConfig = { type: QuestionType.TEXT, label: '', required: false, } as any; const blocks = createQuestionBlocks(question); expect(blocks).toHaveLength(2); const [titleBlock, inputBlock] = blocks; expect(titleBlock.title).toBe(''); expect(inputBlock.title).toBe(''); }); // Test default fallback behavior it('handles unknown question type gracefully', () => { const unknownTypeQuestion = { type: 'UNKNOWN_TYPE' as QuestionType, label: 'Unknown type question', required: false, } as any; const blocks = createQuestionBlocks(unknownTypeQuestion); const inputBlock = blocks.find(b => b.type !== 'TITLE'); // Should fallback to INPUT_TEXT expect(inputBlock?.type).toBe('INPUT_TEXT'); }); }); describe('buildBlocksForForm', () => { it('builds complete form with title and question blocks', () => { const formConfig: FormConfig = { title: 'Test Form', description: 'A test form', questions: [ { type: QuestionType.TEXT, label: 'Name', required: true, }, { type: QuestionType.EMAIL, label: 'Email', required: true, }, ], } as any; const blocks = buildBlocksForForm(formConfig); // Should have: 1 FORM_TITLE + 2 TITLE + 2 INPUT blocks = 5 total expect(blocks).toHaveLength(5); // First block should be FORM_TITLE expect(blocks[0].type).toBe('FORM_TITLE'); expect(blocks[0].title).toBe('Test Form'); // Subsequent blocks should be question blocks expect(blocks[1].type).toBe('TITLE'); expect(blocks[2].type).toBe('INPUT_TEXT'); expect(blocks[3].type).toBe('TITLE'); expect(blocks[4].type).toBe('INPUT_EMAIL'); }); it('handles form with no questions', () => { const formConfig: FormConfig = { title: 'Empty Form', description: 'A form with no questions', questions: [], } as any; const blocks = buildBlocksForForm(formConfig); // Should only have the FORM_TITLE block expect(blocks).toHaveLength(1); expect(blocks[0].type).toBe('FORM_TITLE'); }); it('preserves question order in generated blocks', () => { const formConfig: FormConfig = { title: 'Ordered Form', description: 'Test question ordering', questions: [ { type: QuestionType.TEXT, label: 'First' }, { type: QuestionType.EMAIL, label: 'Second' }, { type: QuestionType.NUMBER, label: 'Third' }, ], } as any; const blocks = buildBlocksForForm(formConfig); // Extract question titles (excluding FORM_TITLE) const questionTitles = blocks .slice(1) // Skip FORM_TITLE .filter(b => b.type === 'TITLE') .map(b => b.title); expect(questionTitles).toEqual(['First', 'Second', 'Third']); }); it('handles form with option-based questions', () => { const formConfig: FormConfig = { title: 'Form with Options', description: 'Test form with dropdown', questions: [ { type: QuestionType.DROPDOWN, label: 'Choose option', options: [{ text: 'A' }, { text: 'B' }], }, ], } as any; const blocks = buildBlocksForForm(formConfig); // Should have: 1 FORM_TITLE + 1 TITLE + 2 DROPDOWN_OPTION = 4 total expect(blocks).toHaveLength(4); expect(blocks[0].type).toBe('FORM_TITLE'); expect(blocks[1].type).toBe('TITLE'); expect(blocks[2].type).toBe('DROPDOWN_OPTION'); expect(blocks[3].type).toBe('DROPDOWN_OPTION'); }); it('validates all blocks have proper UUID format', () => { const formConfig: FormConfig = { title: 'UUID Test Form', description: 'Test UUID generation', questions: [ { type: QuestionType.TEXT, label: 'Test field' }, ], } as any; const blocks = buildBlocksForForm(formConfig); blocks.forEach(block => { expect(block.uuid).toMatch(UUID_V4_REGEX); expect(block.groupUuid).toMatch(UUID_V4_REGEX); }); }); }); describe('Error handling and edge cases', () => { it('handles null/undefined inputs gracefully', () => { // These tests ensure the functions don't crash with invalid inputs expect(() => createFormTitleBlock(null as any)).not.toThrow(); expect(() => createFormTitleBlock(undefined as any)).not.toThrow(); }); it('validates all required block properties are present', () => { const block = createFormTitleBlock('Test'); const requiredProperties = ['uuid', 'type', 'groupUuid', 'groupType', 'title', 'payload']; requiredProperties.forEach(prop => { expect(block).toHaveProperty(prop); }); }); it('ensures UUIDs are properly formatted and unique', () => { const blocks = Array.from({ length: 10 }, () => createFormTitleBlock('Test')); const uuids = blocks.map(b => b.uuid); const groupUuids = blocks.map(b => b.groupUuid); // All UUIDs should be unique expect(new Set(uuids).size).toBe(10); expect(new Set(groupUuids).size).toBe(10); // All UUIDs should match v4 format uuids.forEach(uuid => expect(uuid).toMatch(UUID_V4_REGEX)); groupUuids.forEach(uuid => expect(uuid).toMatch(UUID_V4_REGEX)); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server