@ic-reactor/candid 3.0.12-beta.0 → 3.0.14-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,15 @@
1
1
  import { describe, it, expect } from "vitest"
2
2
  import { IDL } from "@icp-sdk/core/candid"
3
- import { ArgumentFieldVisitor, VectorField } from "./index"
3
+ import {
4
+ FieldVisitor,
5
+ OptionalField,
6
+ RecordField,
7
+ VariantField,
8
+ VectorField,
9
+ } from "./index"
4
10
 
5
11
  describe("ArgumentFieldVisitor", () => {
6
- const visitor = new ArgumentFieldVisitor()
12
+ const visitor = new FieldVisitor()
7
13
 
8
14
  // ════════════════════════════════════════════════════════════════════════
9
15
  // Primitive Types
@@ -186,7 +192,9 @@ describe("ArgumentFieldVisitor", () => {
186
192
  expect(field.type).toBe("record")
187
193
  expect(field.fields).toHaveLength(2)
188
194
 
189
- const addressField = field.fields.find((f) => f.label === "address")
195
+ const addressField = field.fields.find(
196
+ (f) => f.label === "address"
197
+ ) as RecordField
190
198
  if (!addressField || addressField.type !== "record") {
191
199
  throw new Error("Address field not found or not record")
192
200
  }
@@ -235,7 +243,7 @@ describe("ArgumentFieldVisitor", () => {
235
243
  expect(field.fields).toHaveLength(5)
236
244
 
237
245
  // Check 'to' field
238
- const toField = field.fields.find((f) => f.label === "to")
246
+ const toField = field.fields.find((f) => f.label === "to") as RecordField
239
247
  if (!toField || toField.type !== "record") {
240
248
  throw new Error("To field not found or not record")
241
249
  }
@@ -251,7 +259,9 @@ describe("ArgumentFieldVisitor", () => {
251
259
  expect(amountField.candidType).toBe("nat")
252
260
 
253
261
  // Check optional 'fee' field
254
- const feeField = field.fields.find((f) => f.label === "fee")
262
+ const feeField = field.fields.find(
263
+ (f) => f.label === "fee"
264
+ ) as OptionalField
255
265
  if (!feeField || feeField.type !== "optional") {
256
266
  throw new Error("Fee field not found or not optional")
257
267
  }
@@ -289,8 +299,8 @@ describe("ArgumentFieldVisitor", () => {
289
299
  })
290
300
 
291
301
  // Test getOptionDefault helper
292
- expect(field.getOptionDefault("Active")).toEqual({ Active: null })
293
- expect(field.getOptionDefault("Pending")).toEqual({ Pending: null })
302
+ expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
303
+ expect(field.getOptionDefault("Pending")).toEqual({ _type: "Pending" })
294
304
  })
295
305
 
296
306
  it("should handle variant with payloads", () => {
@@ -330,7 +340,9 @@ describe("ArgumentFieldVisitor", () => {
330
340
  expect(field.type).toBe("variant")
331
341
  expect(field.options).toEqual(["Approve", "Burn", "Transfer"]) // Sorted order
332
342
 
333
- const transferField = field.fields.find((f) => f.label === "Transfer")
343
+ const transferField = field.fields.find(
344
+ (f) => f.label === "Transfer"
345
+ ) as RecordField
334
346
  if (!transferField || transferField.type !== "record") {
335
347
  throw new Error("Transfer field not found or not record")
336
348
  }
@@ -429,7 +441,7 @@ describe("ArgumentFieldVisitor", () => {
429
441
  expect(field.type).toBe("optional")
430
442
  expect(field.innerField.type).toBe("record")
431
443
  expect(field.innerField.type).toBe("record")
432
- const inner = field.innerField
444
+ const inner = field.innerField as RecordField
433
445
  if (inner.type === "record") {
434
446
  expect(inner.fields).toHaveLength(2)
435
447
  } else {
@@ -448,7 +460,7 @@ describe("ArgumentFieldVisitor", () => {
448
460
  expect(field.type).toBe("optional")
449
461
  expect(field.innerField.type).toBe("optional")
450
462
  expect(field.innerField.type).toBe("optional")
451
- const inner = field.innerField
463
+ const inner = field.innerField as OptionalField
452
464
  if (inner.type === "optional") {
453
465
  expect(inner.innerField.type).toBe("text")
454
466
  } else {
@@ -479,7 +491,7 @@ describe("ArgumentFieldVisitor", () => {
479
491
 
480
492
  expect(field.type).toBe("vector")
481
493
  expect(field.itemField.type).toBe("record")
482
- const item = field.itemField
494
+ const item = field.itemField as RecordField
483
495
  if (item.type === "record") {
484
496
  expect(item.fields).toHaveLength(2)
485
497
  } else {
@@ -509,7 +521,7 @@ describe("ArgumentFieldVisitor", () => {
509
521
 
510
522
  expect(field.type).toBe("vector")
511
523
  expect(field.itemField.type).toBe("vector")
512
- const item = field.itemField
524
+ const item = field.itemField as VectorField
513
525
  if (item.type === "vector") {
514
526
  expect(item.itemField.type).toBe("text")
515
527
  } else {
@@ -550,7 +562,7 @@ describe("ArgumentFieldVisitor", () => {
550
562
  expect(typeof field.getInnerDefault).toBe("function")
551
563
 
552
564
  // Extract should return a variant
553
- const extracted = field.extract()
565
+ const extracted = field.extract() as VariantField
554
566
  if (extracted.type !== "variant") {
555
567
  throw new Error("Extracted field is not variant")
556
568
  }
@@ -585,14 +597,16 @@ describe("ArgumentFieldVisitor", () => {
585
597
 
586
598
  expect(field.type).toBe("recursive")
587
599
 
588
- const extracted = field.extract()
600
+ const extracted = field.extract() as VariantField
589
601
  if (extracted.type !== "variant") {
590
602
  throw new Error("Extracted field is not variant")
591
603
  }
592
604
  expect(extracted.type).toBe("variant")
593
605
  expect(extracted.options).toEqual(["Nil", "Cons"])
594
606
 
595
- const consField = extracted.fields.find((f) => f.label === "Cons")
607
+ const consField = extracted.fields.find(
608
+ (f) => f.label === "Cons"
609
+ ) as RecordField
596
610
  if (!consField || consField.type !== "record") {
597
611
  throw new Error("Cons field not found or not record")
598
612
  }
@@ -642,7 +656,7 @@ describe("ArgumentFieldVisitor", () => {
642
656
  expect(meta.fields).toHaveLength(1)
643
657
  expect(meta.fields[0].type).toBe("record")
644
658
 
645
- const recordField = meta.fields[0]
659
+ const recordField = meta.fields[0] as RecordField
646
660
  if (recordField.type !== "record") {
647
661
  throw new Error("Expected record field")
648
662
  }
@@ -754,13 +768,15 @@ describe("ArgumentFieldVisitor", () => {
754
768
  )
755
769
  const meta = visitor.visitFunc(funcType, "updateUser")
756
770
 
757
- const argRecord = meta.fields[0]
771
+ const argRecord = meta.fields[0] as RecordField
758
772
  if (argRecord.type !== "record") {
759
773
  throw new Error("Expected record field")
760
774
  }
761
775
  expect(argRecord.name).toBe("[0]")
762
776
 
763
- const userRecord = argRecord.fields.find((f) => f.label === "user")
777
+ const userRecord = argRecord.fields.find(
778
+ (f) => f.label === "user"
779
+ ) as RecordField
764
780
  if (!userRecord || userRecord.type !== "record") {
765
781
  throw new Error("User record not found or not record")
766
782
  }
@@ -783,7 +799,7 @@ describe("ArgumentFieldVisitor", () => {
783
799
  const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
784
800
  const meta = visitor.visitFunc(funcType, "addTags")
785
801
 
786
- const vecField = meta.fields[0]
802
+ const vecField = meta.fields[0] as VectorField
787
803
  if (vecField.type !== "vector") {
788
804
  throw new Error("Expected vector field")
789
805
  }
@@ -837,7 +853,9 @@ describe("ArgumentFieldVisitor", () => {
837
853
  expect(field.fields.length).toBeGreaterThan(5)
838
854
 
839
855
  // Check spender field
840
- const spenderField = field.fields.find((f) => f.label === "spender")
856
+ const spenderField = field.fields.find(
857
+ (f) => f.label === "spender"
858
+ ) as RecordField
841
859
  if (!spenderField || spenderField.type !== "record") {
842
860
  throw new Error("Spender field not found or not record")
843
861
  }
@@ -906,7 +924,9 @@ describe("ArgumentFieldVisitor", () => {
906
924
  expect(field.options).toContain("UpgradeSnsControlledCanister")
907
925
 
908
926
  // Check Motion variant
909
- const motionField = field.fields.find((f) => f.label === "Motion")
927
+ const motionField = field.fields.find(
928
+ (f) => f.label === "Motion"
929
+ ) as RecordField
910
930
  if (!motionField || motionField.type !== "record") {
911
931
  throw new Error("Motion field not found or not record")
912
932
  }
@@ -916,7 +936,7 @@ describe("ArgumentFieldVisitor", () => {
916
936
  // Check TransferSnsTreasuryFunds variant
917
937
  const transferField = field.fields.find(
918
938
  (f) => f.label === "TransferSnsTreasuryFunds"
919
- )
939
+ ) as RecordField
920
940
  if (!transferField || transferField.type !== "record") {
921
941
  throw new Error("Transfer field not found or not record")
922
942
  }
@@ -944,8 +964,9 @@ describe("ArgumentFieldVisitor", () => {
944
964
  "status"
945
965
  )
946
966
 
947
- expect(field.getOptionDefault("Active")).toEqual({ Active: null })
967
+ expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
948
968
  expect(field.getOptionDefault("Pending")).toEqual({
969
+ _type: "Pending",
949
970
  Pending: { reason: "" },
950
971
  })
951
972
  })
@@ -972,4 +993,403 @@ describe("ArgumentFieldVisitor", () => {
972
993
  expect(field.getInnerDefault()).toEqual({ value: "" })
973
994
  })
974
995
  })
996
+
997
+ // ════════════════════════════════════════════════════════════════════════
998
+ // New Features - displayLabel, component, renderHint
999
+ // ════════════════════════════════════════════════════════════════════════
1000
+
1001
+ describe("displayLabel formatting", () => {
1002
+ it("should format __arg labels correctly", () => {
1003
+ const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
1004
+ const meta = visitor.visitFunc(funcType, "test")
1005
+
1006
+ expect(meta.fields[0].displayLabel).toBe("Arg 0")
1007
+ expect(meta.fields[1].displayLabel).toBe("Arg 1")
1008
+ })
1009
+
1010
+ it("should format tuple index labels correctly", () => {
1011
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat, IDL.Bool)
1012
+ const field = visitor.visitTuple(
1013
+ tupleType,
1014
+ [IDL.Text, IDL.Nat, IDL.Bool],
1015
+ "triple"
1016
+ )
1017
+
1018
+ expect(field.fields[0].displayLabel).toBe("Item 0")
1019
+ expect(field.fields[1].displayLabel).toBe("Item 1")
1020
+ expect(field.fields[2].displayLabel).toBe("Item 2")
1021
+ })
1022
+
1023
+ it("should format snake_case labels correctly", () => {
1024
+ const recordType = IDL.Record({
1025
+ created_at_time: IDL.Nat,
1026
+ user_address: IDL.Text,
1027
+ })
1028
+ const field = visitor.visitRecord(
1029
+ recordType,
1030
+ [
1031
+ ["created_at_time", IDL.Nat],
1032
+ ["user_address", IDL.Text],
1033
+ ],
1034
+ "record"
1035
+ )
1036
+
1037
+ const createdField = field.fields.find(
1038
+ (f) => f.label === "created_at_time"
1039
+ )
1040
+ const userField = field.fields.find((f) => f.label === "user_address")
1041
+
1042
+ expect(createdField?.displayLabel).toBe("Created At Time")
1043
+ expect(userField?.displayLabel).toBe("User Address")
1044
+ })
1045
+ })
1046
+
1047
+ describe("component hints", () => {
1048
+ it("should have correct component for record", () => {
1049
+ const recordType = IDL.Record({ name: IDL.Text })
1050
+ const field = visitor.visitRecord(
1051
+ recordType,
1052
+ [["name", IDL.Text]],
1053
+ "person"
1054
+ )
1055
+
1056
+ expect(field.component).toBe("record-container")
1057
+ })
1058
+
1059
+ it("should have correct component for variant", () => {
1060
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text })
1061
+ const field = visitor.visitVariant(
1062
+ variantType,
1063
+ [
1064
+ ["A", IDL.Null],
1065
+ ["B", IDL.Text],
1066
+ ],
1067
+ "choice"
1068
+ )
1069
+
1070
+ expect(field.component).toBe("variant-select")
1071
+ })
1072
+
1073
+ it("should have correct component for optional", () => {
1074
+ const optType = IDL.Opt(IDL.Text)
1075
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1076
+
1077
+ expect(field.component).toBe("optional-toggle")
1078
+ })
1079
+
1080
+ it("should have correct component for vector", () => {
1081
+ const vecType = IDL.Vec(IDL.Text)
1082
+ const field = visitor.visitVec(vecType, IDL.Text, "vec") as VectorField
1083
+
1084
+ expect(field.component).toBe("vector-list")
1085
+ })
1086
+
1087
+ it("should have correct component for blob", () => {
1088
+ const blobType = IDL.Vec(IDL.Nat8)
1089
+ const field = visitor.visitVec(blobType, IDL.Nat8, "blob")
1090
+
1091
+ expect(field.component).toBe("blob-upload")
1092
+ })
1093
+
1094
+ it("should have correct component for text", () => {
1095
+ const field = visitor.visitText(IDL.Text, "text")
1096
+
1097
+ expect(field.component).toBe("text-input")
1098
+ })
1099
+
1100
+ it("should have correct component for number", () => {
1101
+ const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "num")
1102
+
1103
+ expect(field.component).toBe("number-input")
1104
+ })
1105
+
1106
+ it("should have correct component for boolean", () => {
1107
+ const field = visitor.visitBool(IDL.Bool, "bool")
1108
+
1109
+ expect(field.component).toBe("boolean-checkbox")
1110
+ })
1111
+
1112
+ it("should have correct component for principal", () => {
1113
+ const field = visitor.visitPrincipal(IDL.Principal, "principal")
1114
+
1115
+ expect(field.component).toBe("principal-input")
1116
+ })
1117
+
1118
+ it("should have correct component for null", () => {
1119
+ const field = visitor.visitNull(IDL.Null, "null")
1120
+
1121
+ expect(field.component).toBe("null-hidden")
1122
+ })
1123
+ })
1124
+
1125
+ describe("renderHint properties", () => {
1126
+ it("compound types should have isCompound: true", () => {
1127
+ const recordField = visitor.visitRecord(
1128
+ IDL.Record({ x: IDL.Text }),
1129
+ [["x", IDL.Text]],
1130
+ "rec"
1131
+ )
1132
+ const variantField = visitor.visitVariant(
1133
+ IDL.Variant({ A: IDL.Null }),
1134
+ [["A", IDL.Null]],
1135
+ "var"
1136
+ )
1137
+ const optionalField = visitor.visitOpt(IDL.Opt(IDL.Text), IDL.Text, "opt")
1138
+ const vectorField = visitor.visitVec(IDL.Vec(IDL.Text), IDL.Text, "vec")
1139
+
1140
+ expect(recordField.renderHint.isCompound).toBe(true)
1141
+ expect(recordField.renderHint.isPrimitive).toBe(false)
1142
+
1143
+ expect(variantField.renderHint.isCompound).toBe(true)
1144
+ expect(optionalField.renderHint.isCompound).toBe(true)
1145
+ expect((vectorField as VectorField).renderHint.isCompound).toBe(true)
1146
+ })
1147
+
1148
+ it("primitive types should have isPrimitive: true", () => {
1149
+ const textField = visitor.visitText(IDL.Text, "text")
1150
+ const boolField = visitor.visitBool(IDL.Bool, "bool")
1151
+ const principalField = visitor.visitPrincipal(IDL.Principal, "principal")
1152
+
1153
+ expect(textField.renderHint.isPrimitive).toBe(true)
1154
+ expect(textField.renderHint.isCompound).toBe(false)
1155
+
1156
+ expect(boolField.renderHint.isPrimitive).toBe(true)
1157
+ expect(principalField.renderHint.isPrimitive).toBe(true)
1158
+ })
1159
+
1160
+ it("should have correct inputType hints", () => {
1161
+ const textField = visitor.visitText(IDL.Text, "text")
1162
+ const boolField = visitor.visitBool(IDL.Bool, "bool")
1163
+ const numField = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "num")
1164
+
1165
+ expect(textField.renderHint.inputType).toBe("text")
1166
+ expect(boolField.renderHint.inputType).toBe("checkbox")
1167
+ expect(numField.renderHint.inputType).toBe("number")
1168
+ })
1169
+ })
1170
+
1171
+ describe("inputProps for primitive types", () => {
1172
+ it("text field should have inputProps", () => {
1173
+ const field = visitor.visitText(IDL.Text, "text")
1174
+
1175
+ expect(field.inputProps).toBeDefined()
1176
+ expect(field.inputProps.type).toBe("text")
1177
+ expect(field.inputProps.placeholder).toBeDefined()
1178
+ })
1179
+
1180
+ it("boolean field should have checkbox inputProps", () => {
1181
+ const field = visitor.visitBool(IDL.Bool, "bool")
1182
+
1183
+ expect(field.inputProps).toBeDefined()
1184
+ expect(field.inputProps.type).toBe("checkbox")
1185
+ })
1186
+
1187
+ it("number field should have number inputProps with min/max", () => {
1188
+ const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
1189
+
1190
+ if (field.type === "number") {
1191
+ expect(field.inputProps).toBeDefined()
1192
+ expect(field.inputProps.type).toBe("number")
1193
+ expect(field.inputProps.min).toBe("0")
1194
+ expect(field.inputProps.max).toBe("255")
1195
+ }
1196
+ })
1197
+
1198
+ it("principal field should have inputProps with spellCheck disabled", () => {
1199
+ const field = visitor.visitPrincipal(IDL.Principal, "principal")
1200
+
1201
+ expect(field.inputProps).toBeDefined()
1202
+ expect(field.inputProps.spellCheck).toBe(false)
1203
+ expect(field.inputProps.autoComplete).toBe("off")
1204
+ })
1205
+ })
1206
+
1207
+ // ════════════════════════════════════════════════════════════════════════
1208
+ // Enhanced Variant Helpers
1209
+ // ════════════════════════════════════════════════════════════════════════
1210
+
1211
+ describe("Variant helper methods", () => {
1212
+ it("getField should return the correct field for an option", () => {
1213
+ const variantType = IDL.Variant({
1214
+ Transfer: IDL.Record({ to: IDL.Principal, amount: IDL.Nat }),
1215
+ Burn: IDL.Nat,
1216
+ })
1217
+ const field = visitor.visitVariant(
1218
+ variantType,
1219
+ [
1220
+ ["Transfer", IDL.Record({ to: IDL.Principal, amount: IDL.Nat })],
1221
+ ["Burn", IDL.Nat],
1222
+ ],
1223
+ "action"
1224
+ )
1225
+
1226
+ const transferField = field.getField("Transfer")
1227
+ expect(transferField.type).toBe("record")
1228
+
1229
+ const burnField = field.getField("Burn")
1230
+ expect(burnField.type).toBe("text") // nat is rendered as text for large numbers
1231
+ })
1232
+
1233
+ it("getSelectedOption should return the selected option from a value", () => {
1234
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text, C: IDL.Nat })
1235
+ const field = visitor.visitVariant(
1236
+ variantType,
1237
+ [
1238
+ ["A", IDL.Null],
1239
+ ["B", IDL.Text],
1240
+ ["C", IDL.Nat],
1241
+ ],
1242
+ "choice"
1243
+ )
1244
+
1245
+ expect(field.getSelectedOption({ A: null })).toBe("A")
1246
+ expect(field.getSelectedOption({ B: "hello" })).toBe("B")
1247
+ expect(field.getSelectedOption({ C: "100" })).toBe("C")
1248
+ // Falls back to default option for unknown values
1249
+ expect(field.getSelectedOption({})).toBe("A")
1250
+ })
1251
+
1252
+ it("getSelectedField should return the field for the selected option", () => {
1253
+ const variantType = IDL.Variant({
1254
+ Ok: IDL.Nat,
1255
+ Err: IDL.Text,
1256
+ })
1257
+ const field = visitor.visitVariant(
1258
+ variantType,
1259
+ [
1260
+ ["Ok", IDL.Nat],
1261
+ ["Err", IDL.Text],
1262
+ ],
1263
+ "result"
1264
+ )
1265
+
1266
+ const okField = field.getSelectedField({ Ok: "100" })
1267
+ expect(okField.label).toBe("Ok")
1268
+
1269
+ const errField = field.getSelectedField({ Err: "error" })
1270
+ expect(errField.label).toBe("Err")
1271
+ })
1272
+ })
1273
+
1274
+ // ════════════════════════════════════════════════════════════════════════
1275
+ // Optional isEnabled Helper
1276
+ // ════════════════════════════════════════════════════════════════════════
1277
+
1278
+ describe("Optional isEnabled helper", () => {
1279
+ it("should return true for non-null values", () => {
1280
+ const optType = IDL.Opt(IDL.Text)
1281
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1282
+
1283
+ expect(field.isEnabled("hello")).toBe(true)
1284
+ expect(field.isEnabled("")).toBe(true)
1285
+ expect(field.isEnabled(0)).toBe(true)
1286
+ expect(field.isEnabled(false)).toBe(true)
1287
+ expect(field.isEnabled({})).toBe(true)
1288
+ })
1289
+
1290
+ it("should return false for null and undefined", () => {
1291
+ const optType = IDL.Opt(IDL.Text)
1292
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1293
+
1294
+ expect(field.isEnabled(null)).toBe(false)
1295
+ expect(field.isEnabled(undefined)).toBe(false)
1296
+ })
1297
+ })
1298
+
1299
+ // ════════════════════════════════════════════════════════════════════════
1300
+ // Vector createItemField Helper
1301
+ // ════════════════════════════════════════════════════════════════════════
1302
+
1303
+ describe("Vector createItemField helper", () => {
1304
+ it("should create item field with correct index in name path", () => {
1305
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
1306
+ const meta = visitor.visitFunc(funcType, "addItems")
1307
+ const vecField = meta.fields[0] as VectorField
1308
+
1309
+ const item0 = vecField.createItemField(0)
1310
+ expect(item0.name).toBe("[0][0]")
1311
+
1312
+ const item5 = vecField.createItemField(5)
1313
+ expect(item5.name).toBe("[0][5]")
1314
+ })
1315
+
1316
+ it("should use custom label when provided", () => {
1317
+ const funcType = IDL.Func(
1318
+ [IDL.Vec(IDL.Record({ name: IDL.Text }))],
1319
+ [],
1320
+ []
1321
+ )
1322
+ const meta = visitor.visitFunc(funcType, "addItems")
1323
+ const vecField = meta.fields[0] as VectorField
1324
+
1325
+ const item = vecField.createItemField(3, { label: "Person 3" })
1326
+ expect(item.label).toBe("Person 3")
1327
+ expect(item.displayLabel).toBe("Person 3")
1328
+ })
1329
+
1330
+ it("should use default label when not provided", () => {
1331
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
1332
+ const meta = visitor.visitFunc(funcType, "addTags")
1333
+ const vecField = meta.fields[0] as VectorField
1334
+
1335
+ const item = vecField.createItemField(2)
1336
+ expect(item.label).toBe("Item 2")
1337
+ expect(item.displayLabel).toBe("Item 2")
1338
+ })
1339
+ })
1340
+
1341
+ // ════════════════════════════════════════════════════════════════════════
1342
+ // Blob Field Utilities
1343
+ // ════════════════════════════════════════════════════════════════════════
1344
+
1345
+ describe("Blob field utilities", () => {
1346
+ it("should have limits defined", () => {
1347
+ const blobType = IDL.Vec(IDL.Nat8)
1348
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1349
+
1350
+ if (field.type === "blob") {
1351
+ expect(field.limits).toBeDefined()
1352
+ expect(field.limits.maxHexBytes).toBeGreaterThan(0)
1353
+ expect(field.limits.maxFileBytes).toBeGreaterThan(0)
1354
+ expect(field.limits.maxHexDisplayLength).toBeGreaterThan(0)
1355
+ }
1356
+ })
1357
+
1358
+ it("normalizeHex should remove 0x prefix and lowercase", () => {
1359
+ const blobType = IDL.Vec(IDL.Nat8)
1360
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1361
+
1362
+ if (field.type === "blob") {
1363
+ expect(field.normalizeHex("0xDEADBEEF")).toBe("deadbeef")
1364
+ expect(field.normalizeHex("DEADBEEF")).toBe("deadbeef")
1365
+ expect(field.normalizeHex("0x")).toBe("")
1366
+ expect(field.normalizeHex("abc123")).toBe("abc123")
1367
+ }
1368
+ })
1369
+
1370
+ it("validateInput should validate hex strings", () => {
1371
+ const blobType = IDL.Vec(IDL.Nat8)
1372
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1373
+
1374
+ if (field.type === "blob") {
1375
+ // Valid inputs
1376
+ expect(field.validateInput("").valid).toBe(true)
1377
+ expect(field.validateInput("deadbeef").valid).toBe(true)
1378
+ expect(field.validateInput("0x1234").valid).toBe(true)
1379
+
1380
+ // Invalid inputs
1381
+ expect(field.validateInput("xyz").valid).toBe(false)
1382
+ expect(field.validateInput("abc").valid).toBe(false) // odd length
1383
+ }
1384
+ })
1385
+
1386
+ it("validateInput should validate Uint8Array", () => {
1387
+ const blobType = IDL.Vec(IDL.Nat8)
1388
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1389
+
1390
+ if (field.type === "blob") {
1391
+ expect(field.validateInput(new Uint8Array([1, 2, 3])).valid).toBe(true)
1392
+ }
1393
+ })
1394
+ })
975
1395
  })