|
1 | 1 | import { renderHook, act } from '@testing-library/react'; |
2 | 2 | import { useLayerStore } from '@/lib/ui-builder/store/layer-store'; |
3 | | -import { ComponentLayer } from '@/components/ui/ui-builder/types'; |
| 3 | +import { ComponentLayer, Variable } from '@/components/ui/ui-builder/types'; |
4 | 4 | import { z } from 'zod'; |
5 | 5 |
|
6 | 6 | import { useEditorStore } from '@/lib/ui-builder/store/editor-store'; |
@@ -60,6 +60,13 @@ describe('LayerStore', () => { |
60 | 60 | } |
61 | 61 | ], |
62 | 62 | }, |
| 63 | + ComponentWithoutBindings: { |
| 64 | + schema: z.object({ |
| 65 | + text: z.string().default('Default Text'), |
| 66 | + }), |
| 67 | + from: '@/components/ui/component-without-bindings', |
| 68 | + component: () => null, |
| 69 | + }, |
63 | 70 | // Add other components as needed with appropriate Zod schemas |
64 | 71 | } |
65 | 72 | }); |
@@ -869,6 +876,260 @@ describe('LayerStore', () => { |
869 | 876 |
|
870 | 877 | expect(result.current.variables).toEqual(variables); |
871 | 878 | }); |
| 879 | + |
| 880 | + it('should initialize immutable bindings for existing layers with variable references', () => { |
| 881 | + const { result } = renderHook(() => useLayerStore()); |
| 882 | + |
| 883 | + // Create layers with existing variable references |
| 884 | + const initialLayers: ComponentLayer[] = [ |
| 885 | + { |
| 886 | + id: 'page1', |
| 887 | + type: 'div', |
| 888 | + name: 'Page 1', |
| 889 | + props: { className: 'p-4' }, |
| 890 | + children: [ |
| 891 | + { |
| 892 | + id: 'component-with-bindings', |
| 893 | + type: 'ComponentWithDefaultBindings', |
| 894 | + name: 'Test Component', |
| 895 | + props: { |
| 896 | + title: { __variableRef: 'var-id-1' }, // This should be immutable |
| 897 | + description: { __variableRef: 'var-id-2' }, // This should be mutable |
| 898 | + count: 42 |
| 899 | + }, |
| 900 | + children: [] |
| 901 | + } |
| 902 | + ] |
| 903 | + } |
| 904 | + ]; |
| 905 | + |
| 906 | + const initialVariables: Variable[] = [ |
| 907 | + { id: 'var-id-1', name: 'Title Variable', type: 'string', defaultValue: 'Test Title' }, |
| 908 | + { id: 'var-id-2', name: 'Description Variable', type: 'string', defaultValue: 'Test Description' }, |
| 909 | + ]; |
| 910 | + |
| 911 | + // Update registry to use the correct variable IDs |
| 912 | + useEditorStore.setState({ |
| 913 | + registry: { |
| 914 | + ...useEditorStore.getState().registry, |
| 915 | + ComponentWithDefaultBindings: { |
| 916 | + schema: z.object({ |
| 917 | + title: z.string().default('Default Title'), |
| 918 | + description: z.string().default('Default Description'), |
| 919 | + count: z.number().default(0), |
| 920 | + }), |
| 921 | + from: '@/components/ui/component-with-default-bindings', |
| 922 | + component: () => null, |
| 923 | + defaultVariableBindings: [ |
| 924 | + { propName: 'title', variableId: 'var-id-1', immutable: true }, |
| 925 | + { propName: 'description', variableId: 'var-id-2', immutable: false }, |
| 926 | + ], |
| 927 | + }, |
| 928 | + } |
| 929 | + }); |
| 930 | + |
| 931 | + // Initialize with the layers that already have variable bindings |
| 932 | + act(() => { |
| 933 | + result.current.initialize(initialLayers, 'page1', undefined, initialVariables); |
| 934 | + }); |
| 935 | + |
| 936 | + // Check that immutable bindings were properly set up during initialization |
| 937 | + expect(result.current.isBindingImmutable('component-with-bindings', 'title')).toBe(true); |
| 938 | + expect(result.current.isBindingImmutable('component-with-bindings', 'description')).toBe(false); |
| 939 | + expect(result.current.isBindingImmutable('component-with-bindings', 'count')).toBe(false); |
| 940 | + |
| 941 | + // Verify that the layer was initialized correctly |
| 942 | + const component = result.current.findLayerById('component-with-bindings'); |
| 943 | + expect(component?.props.title).toEqual({ __variableRef: 'var-id-1' }); |
| 944 | + expect(component?.props.description).toEqual({ __variableRef: 'var-id-2' }); |
| 945 | + expect(component?.props.count).toBe(42); |
| 946 | + |
| 947 | + // Test that immutable binding prevents unbinding |
| 948 | + act(() => { |
| 949 | + result.current.unbindPropFromVariable('component-with-bindings', 'title'); |
| 950 | + }); |
| 951 | + |
| 952 | + // Should still be bound (immutable) |
| 953 | + const componentAfterUnbind = result.current.findLayerById('component-with-bindings'); |
| 954 | + expect(componentAfterUnbind?.props.title).toEqual({ __variableRef: 'var-id-1' }); |
| 955 | + |
| 956 | + // Test that mutable binding allows unbinding |
| 957 | + act(() => { |
| 958 | + result.current.unbindPropFromVariable('component-with-bindings', 'description'); |
| 959 | + }); |
| 960 | + |
| 961 | + // Should be unbound (mutable) |
| 962 | + const componentAfterMutableUnbind = result.current.findLayerById('component-with-bindings'); |
| 963 | + expect(componentAfterMutableUnbind?.props.description).toBe('Default Description'); |
| 964 | + }); |
| 965 | + |
| 966 | + it('should apply default variable bindings when adding a component', () => { |
| 967 | + const { result } = renderHook(() => useLayerStore()); |
| 968 | + |
| 969 | + // Manually set variable IDs to match what we expect in the binding definitions |
| 970 | + act(() => { |
| 971 | + const variables = result.current.variables; |
| 972 | + if (variables.length >= 2) { |
| 973 | + // Update the registry to use actual variable IDs |
| 974 | + const registry = useEditorStore.getState().registry; |
| 975 | + useEditorStore.setState({ |
| 976 | + registry: { |
| 977 | + ...registry, |
| 978 | + ComponentWithDefaultBindings: { |
| 979 | + ...registry.ComponentWithDefaultBindings, |
| 980 | + defaultVariableBindings: [ |
| 981 | + { propName: 'title', variableId: variables[0].id, immutable: true }, |
| 982 | + { propName: 'description', variableId: variables[1].id, immutable: false }, |
| 983 | + ], |
| 984 | + }, |
| 985 | + } |
| 986 | + }); |
| 987 | + |
| 988 | + result.current.addComponentLayer('ComponentWithDefaultBindings', '1'); |
| 989 | + } |
| 990 | + }); |
| 991 | + |
| 992 | + const addedLayer = (result.current.pages[0].children[0] as ComponentLayer); |
| 993 | + expect(addedLayer.type).toBe('ComponentWithDefaultBindings'); |
| 994 | + |
| 995 | + // Check that variable bindings were applied |
| 996 | + const variables = result.current.variables; |
| 997 | + expect(addedLayer.props.title).toEqual({ __variableRef: variables[0].id }); |
| 998 | + expect(addedLayer.props.description).toEqual({ __variableRef: variables[1].id }); |
| 999 | + |
| 1000 | + // Check that immutable bindings were tracked |
| 1001 | + expect(result.current.isBindingImmutable(addedLayer.id, 'title')).toBe(true); |
| 1002 | + expect(result.current.isBindingImmutable(addedLayer.id, 'description')).toBe(false); |
| 1003 | + }); |
| 1004 | + |
| 1005 | + it('should not apply bindings for non-existent variables', () => { |
| 1006 | + const { result } = renderHook(() => useLayerStore()); |
| 1007 | + |
| 1008 | + // Use registry with non-existent variable IDs |
| 1009 | + useEditorStore.setState({ |
| 1010 | + registry: { |
| 1011 | + ...useEditorStore.getState().registry, |
| 1012 | + ComponentWithInvalidBindings: { |
| 1013 | + schema: z.object({ |
| 1014 | + title: z.string().default('Default Title'), |
| 1015 | + }), |
| 1016 | + from: '@/components/ui/component-with-invalid-bindings', |
| 1017 | + component: () => null, |
| 1018 | + defaultVariableBindings: [ |
| 1019 | + { propName: 'title', variableId: 'non-existent-var', immutable: true }, |
| 1020 | + ], |
| 1021 | + }, |
| 1022 | + } |
| 1023 | + }); |
| 1024 | + |
| 1025 | + act(() => { |
| 1026 | + result.current.addComponentLayer('ComponentWithInvalidBindings', '1'); |
| 1027 | + }); |
| 1028 | + |
| 1029 | + const addedLayer = (result.current.pages[0].children[0] as ComponentLayer); |
| 1030 | + |
| 1031 | + // Should use default value from schema, not variable binding |
| 1032 | + expect(addedLayer.props.title).toBe('Default Title'); |
| 1033 | + expect(result.current.isBindingImmutable(addedLayer.id, 'title')).toBe(false); |
| 1034 | + }); |
| 1035 | + |
| 1036 | + it('should handle components without default variable bindings', () => { |
| 1037 | + const { result } = renderHook(() => useLayerStore()); |
| 1038 | + |
| 1039 | + act(() => { |
| 1040 | + result.current.addComponentLayer('ComponentWithoutBindings', '1'); |
| 1041 | + }); |
| 1042 | + |
| 1043 | + const addedLayer = (result.current.pages[0].children[0] as ComponentLayer); |
| 1044 | + expect(addedLayer.type).toBe('ComponentWithoutBindings'); |
| 1045 | + expect(addedLayer.props.text).toBe('Default Text'); |
| 1046 | + expect(result.current.isBindingImmutable(addedLayer.id, 'text')).toBe(false); |
| 1047 | + }); |
| 1048 | + |
| 1049 | + it('should prevent unbinding immutable variable bindings', () => { |
| 1050 | + const { result } = renderHook(() => useLayerStore()); |
| 1051 | + |
| 1052 | + // Set up component with immutable binding |
| 1053 | + act(() => { |
| 1054 | + const variables = result.current.variables; |
| 1055 | + if (variables.length >= 1) { |
| 1056 | + useEditorStore.setState({ |
| 1057 | + registry: { |
| 1058 | + ...useEditorStore.getState().registry, |
| 1059 | + ComponentWithDefaultBindings: { |
| 1060 | + ...useEditorStore.getState().registry.ComponentWithDefaultBindings, |
| 1061 | + defaultVariableBindings: [ |
| 1062 | + { propName: 'title', variableId: variables[0].id, immutable: true }, |
| 1063 | + ], |
| 1064 | + }, |
| 1065 | + } |
| 1066 | + }); |
| 1067 | + |
| 1068 | + result.current.addComponentLayer('ComponentWithDefaultBindings', '1'); |
| 1069 | + } |
| 1070 | + }); |
| 1071 | + |
| 1072 | + const addedLayer = (result.current.pages[0].children[0] as ComponentLayer); |
| 1073 | + |
| 1074 | + // Verify binding exists |
| 1075 | + expect(addedLayer.props.title).toEqual({ __variableRef: result.current.variables[0].id }); |
| 1076 | + expect(result.current.isBindingImmutable(addedLayer.id, 'title')).toBe(true); |
| 1077 | + |
| 1078 | + // Try to unbind immutable binding (should fail) |
| 1079 | + act(() => { |
| 1080 | + result.current.unbindPropFromVariable(addedLayer.id, 'title'); |
| 1081 | + }); |
| 1082 | + |
| 1083 | + // Binding should still exist |
| 1084 | + const layerAfterUnbind = result.current.findLayerById(addedLayer.id) as ComponentLayer; |
| 1085 | + expect(layerAfterUnbind.props.title).toEqual({ __variableRef: result.current.variables[0].id }); |
| 1086 | + }); |
| 1087 | + |
| 1088 | + it('should allow unbinding mutable variable bindings', () => { |
| 1089 | + const { result } = renderHook(() => useLayerStore()); |
| 1090 | + |
| 1091 | + // Set up component with mutable binding |
| 1092 | + act(() => { |
| 1093 | + const variables = result.current.variables; |
| 1094 | + if (variables.length >= 1) { |
| 1095 | + useEditorStore.setState({ |
| 1096 | + registry: { |
| 1097 | + ...useEditorStore.getState().registry, |
| 1098 | + ComponentWithDefaultBindings: { |
| 1099 | + ...useEditorStore.getState().registry.ComponentWithDefaultBindings, |
| 1100 | + defaultVariableBindings: [ |
| 1101 | + { propName: 'description', variableId: variables[0].id, immutable: false }, |
| 1102 | + ], |
| 1103 | + }, |
| 1104 | + } |
| 1105 | + }); |
| 1106 | + |
| 1107 | + result.current.addComponentLayer('ComponentWithDefaultBindings', '1'); |
| 1108 | + } |
| 1109 | + }); |
| 1110 | + |
| 1111 | + const addedLayer = (result.current.pages[0].children[0] as ComponentLayer); |
| 1112 | + |
| 1113 | + // Verify binding exists |
| 1114 | + expect(addedLayer.props.description).toEqual({ __variableRef: result.current.variables[0].id }); |
| 1115 | + expect(result.current.isBindingImmutable(addedLayer.id, 'description')).toBe(false); |
| 1116 | + |
| 1117 | + // Unbind mutable binding (should succeed) |
| 1118 | + act(() => { |
| 1119 | + result.current.unbindPropFromVariable(addedLayer.id, 'description'); |
| 1120 | + }); |
| 1121 | + |
| 1122 | + // Binding should be removed and default value set |
| 1123 | + const layerAfterUnbind = result.current.findLayerById(addedLayer.id) as ComponentLayer; |
| 1124 | + expect(layerAfterUnbind.props.description).toBe('Default Description'); |
| 1125 | + }); |
| 1126 | + |
| 1127 | + it('should correctly report binding immutability', () => { |
| 1128 | + const { result } = renderHook(() => useLayerStore()); |
| 1129 | + |
| 1130 | + expect(result.current.isBindingImmutable('non-existent-layer', 'prop')).toBe(false); |
| 1131 | + expect(result.current.isBindingImmutable('layer-id', 'non-existent-prop')).toBe(false); |
| 1132 | + }); |
872 | 1133 | }); |
873 | 1134 |
|
874 | 1135 | describe('Edge Cases and Error Handling', () => { |
|
0 commit comments