import type { CanvasComponent, ProjectDocument, RestAction } from '../../types/project'; import { createResponseBinding, findVariableReferences, parseVariableDefault, removeBindingAt, removeVariable, responseTargetOptions, setVariable, validateVariableName, } from './configurationUtils'; const action: RestAction = { id: 'action_lookup', name: 'Lookup', method: 'GET', url: 'https://example.com', headers: {}, queryParameters: {}, pathParameters: {}, bodyTemplate: '', authenticationType: 'anonymous', }; const components: CanvasComponent[] = [ { id: 'label', type: 'Label', name: 'status', position: { x: 0, y: 0 }, size: { width: 10, height: 10 }, properties: {} }, { id: 'dropdown', type: 'Dropdown', name: 'choices', position: { x: 0, y: 0 }, size: { width: 10, height: 10 }, properties: {} }, { id: 'input', type: 'TextInput', name: 'query', position: { x: 0, y: 0 }, size: { width: 10, height: 10 }, properties: {} }, ]; const doc: ProjectDocument = { schemaVersion: '0.1.0', project: { id: 'p', name: 'P', pages: [], actions: [action], bindings: [], variables: {}, settings: {} }, }; test('creates canonical action-response bindings with onSuccess and a unique ID', () => { const first = createResponseBinding([], action, 'components.status.value'); const second = createResponseBinding([first], action, 'variables.result'); expect(first).toEqual({ id: 'binding_response_1', source: 'actions.action_lookup.response.body', target: 'components.status.value', trigger: 'onSuccess' }); expect(second.id).toBe('binding_response_2'); }); test('offers only runtime-supported component targets plus variables', () => { expect(responseTargetOptions(components, { result: { type: 'object' } })).toEqual([ { value: 'components.status.value', label: 'status (Label.value)' }, { value: 'components.choices.options', label: 'choices (Dropdown.options)' }, { value: 'variables.result', label: 'result (variable)' }, ]); }); test('does not offer ambiguous duplicate component names as new targets', () => { expect(responseTargetOptions([ components[0], { ...components[0], id: 'other-label' }, ], {})).toEqual([]); }); test('parses typed defaults and reports invalid declarations', () => { expect(parseVariableDefault('string', '5')).toBe('5'); expect(parseVariableDefault('number', '5')).toBe(5); expect(parseVariableDefault('boolean', 'false')).toBe(false); expect(parseVariableDefault('array', '[1,2]')).toEqual([1, 2]); expect(() => parseVariableDefault('object', '[]')).toThrow('JSON object'); expect(validateVariableName('bad.name', {})).not.toBeNull(); expect(validateVariableName('result', { result: { type: 'string' } })).toContain('already exists'); }); test('writes and removes variables and bindings without touching other canonical state', () => { const withVariable = setVariable(doc, 'result', { type: 'string', defaultValue: 'ready' }); const binding = createResponseBinding([], action, 'variables.result'); const withBinding = { ...withVariable, project: { ...withVariable.project, bindings: [binding] } }; expect(removeBindingAt(withBinding, 0).project.bindings).toEqual([]); expect(removeVariable(withVariable, 'result').project.variables).toEqual({}); expect(withVariable.project.actions).toBe(doc.project.actions); }); test('finds binding and request-template references before variable deletion', () => { const referenced: ProjectDocument = { ...doc, project: { ...doc.project, actions: [{ ...action, queryParameters: { item: '{{variables.result}}' }, pathParameters: { legacy: '{{variables.result}}' } }], bindings: [{ id: 'b', source: 'actions.action_lookup.response', target: 'variables.result', trigger: 'onSuccess' }], variables: { result: { type: 'string' } }, }, }; expect(findVariableReferences(referenced, 'result')).toEqual(['binding "b"', 'action "Lookup"']); });