@ic-reactor/candid 3.0.2-beta.0 → 3.0.2

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.
Files changed (83) hide show
  1. package/README.md +33 -1
  2. package/dist/adapter.js +2 -1
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/display-reactor.d.ts +4 -13
  5. package/dist/display-reactor.d.ts.map +1 -1
  6. package/dist/display-reactor.js +22 -8
  7. package/dist/display-reactor.js.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/metadata-display-reactor.d.ts +108 -0
  13. package/dist/metadata-display-reactor.d.ts.map +1 -0
  14. package/dist/metadata-display-reactor.js +141 -0
  15. package/dist/metadata-display-reactor.js.map +1 -0
  16. package/dist/reactor.d.ts +1 -1
  17. package/dist/reactor.d.ts.map +1 -1
  18. package/dist/reactor.js +10 -6
  19. package/dist/reactor.js.map +1 -1
  20. package/dist/types.d.ts +38 -7
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/utils.d.ts +4 -4
  23. package/dist/utils.d.ts.map +1 -1
  24. package/dist/utils.js +33 -10
  25. package/dist/utils.js.map +1 -1
  26. package/dist/visitor/arguments/helpers.d.ts +55 -0
  27. package/dist/visitor/arguments/helpers.d.ts.map +1 -0
  28. package/dist/visitor/arguments/helpers.js +123 -0
  29. package/dist/visitor/arguments/helpers.js.map +1 -0
  30. package/dist/visitor/arguments/index.d.ts +101 -0
  31. package/dist/visitor/arguments/index.d.ts.map +1 -0
  32. package/dist/visitor/arguments/index.js +780 -0
  33. package/dist/visitor/arguments/index.js.map +1 -0
  34. package/dist/visitor/arguments/types.d.ts +270 -0
  35. package/dist/visitor/arguments/types.d.ts.map +1 -0
  36. package/dist/visitor/arguments/types.js +26 -0
  37. package/dist/visitor/arguments/types.js.map +1 -0
  38. package/dist/visitor/constants.d.ts +4 -0
  39. package/dist/visitor/constants.d.ts.map +1 -0
  40. package/dist/visitor/constants.js +73 -0
  41. package/dist/visitor/constants.js.map +1 -0
  42. package/dist/visitor/helpers.d.ts +30 -0
  43. package/dist/visitor/helpers.d.ts.map +1 -0
  44. package/dist/visitor/helpers.js +204 -0
  45. package/dist/visitor/helpers.js.map +1 -0
  46. package/dist/visitor/index.d.ts +5 -0
  47. package/dist/visitor/index.d.ts.map +1 -0
  48. package/dist/visitor/index.js +5 -0
  49. package/dist/visitor/index.js.map +1 -0
  50. package/dist/visitor/returns/index.d.ts +38 -0
  51. package/dist/visitor/returns/index.d.ts.map +1 -0
  52. package/dist/visitor/returns/index.js +460 -0
  53. package/dist/visitor/returns/index.js.map +1 -0
  54. package/dist/visitor/returns/types.d.ts +202 -0
  55. package/dist/visitor/returns/types.d.ts.map +1 -0
  56. package/dist/visitor/returns/types.js +2 -0
  57. package/dist/visitor/returns/types.js.map +1 -0
  58. package/dist/visitor/types.d.ts +19 -0
  59. package/dist/visitor/types.d.ts.map +1 -0
  60. package/dist/visitor/types.js +2 -0
  61. package/dist/visitor/types.js.map +1 -0
  62. package/package.json +16 -7
  63. package/src/adapter.ts +446 -0
  64. package/src/constants.ts +11 -0
  65. package/src/display-reactor.ts +337 -0
  66. package/src/index.ts +8 -0
  67. package/src/metadata-display-reactor.ts +230 -0
  68. package/src/reactor.ts +199 -0
  69. package/src/types.ts +127 -0
  70. package/src/utils.ts +60 -0
  71. package/src/visitor/arguments/helpers.ts +153 -0
  72. package/src/visitor/arguments/index.test.ts +1439 -0
  73. package/src/visitor/arguments/index.ts +981 -0
  74. package/src/visitor/arguments/schema.test.ts +324 -0
  75. package/src/visitor/arguments/types.ts +387 -0
  76. package/src/visitor/constants.ts +76 -0
  77. package/src/visitor/helpers.test.ts +274 -0
  78. package/src/visitor/helpers.ts +223 -0
  79. package/src/visitor/index.ts +4 -0
  80. package/src/visitor/returns/index.test.ts +2377 -0
  81. package/src/visitor/returns/index.ts +658 -0
  82. package/src/visitor/returns/types.ts +302 -0
  83. package/src/visitor/types.ts +75 -0
@@ -0,0 +1,1439 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { IDL } from "@icp-sdk/core/candid"
3
+ import {
4
+ FieldVisitor,
5
+ OptionalField,
6
+ RecordField,
7
+ VariantField,
8
+ VectorField,
9
+ } from "./index"
10
+
11
+ describe("ArgumentFieldVisitor", () => {
12
+ const visitor = new FieldVisitor()
13
+
14
+ // ════════════════════════════════════════════════════════════════════════
15
+ // Primitive Types
16
+ // ════════════════════════════════════════════════════════════════════════
17
+
18
+ describe("Primitive Types", () => {
19
+ it("should handle text type", () => {
20
+ const field = visitor.visitText(IDL.Text, "username")
21
+
22
+ expect(field.type).toBe("text")
23
+ expect(field.label).toBe("username")
24
+ expect(field.defaultValue).toBe("")
25
+ })
26
+
27
+ it("should handle bool type", () => {
28
+ const field = visitor.visitBool(IDL.Bool, "isActive")
29
+
30
+ expect(field.type).toBe("boolean")
31
+ expect(field.label).toBe("isActive")
32
+ expect(field.defaultValue).toBe(false)
33
+ })
34
+
35
+ it("should handle null type", () => {
36
+ const field = visitor.visitNull(IDL.Null, "empty")
37
+
38
+ expect(field.type).toBe("null")
39
+ expect(field.label).toBe("empty")
40
+ expect(field.defaultValue).toBe(null)
41
+ })
42
+
43
+ it("should handle principal type", () => {
44
+ const field = visitor.visitPrincipal(IDL.Principal, "caller")
45
+
46
+ expect(field.type).toBe("principal")
47
+ expect(field.label).toBe("caller")
48
+ expect(field.defaultValue).toBe("")
49
+ expect(field.minLength).toBe(7)
50
+ expect(field.maxLength).toBe(64)
51
+ })
52
+ })
53
+
54
+ // ════════════════════════════════════════════════════════════════════════
55
+ // Number Types
56
+ // ════════════════════════════════════════════════════════════════════════
57
+
58
+ describe("Number Types", () => {
59
+ it("should handle nat type", () => {
60
+ const field = visitor.visitNat(IDL.Nat, "amount")
61
+
62
+ expect(field.type).toBe("text")
63
+ expect(field.label).toBe("amount")
64
+ expect(field.candidType).toBe("nat")
65
+ expect(field.defaultValue).toBe("")
66
+ // TextField doesn't have isFloat or unsigned properties in the types
67
+ })
68
+
69
+ it("should handle int type", () => {
70
+ const field = visitor.visitInt(IDL.Int, "balance")
71
+
72
+ expect(field.type).toBe("text")
73
+ expect(field.candidType).toBe("int")
74
+ })
75
+
76
+ it("should handle nat8 type with min/max", () => {
77
+ const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
78
+
79
+ if (field.type === "number") {
80
+ expect(field.type).toBe("number")
81
+ expect(field.candidType).toBe("nat8")
82
+ expect(field.bits).toBe(8)
83
+ expect(field.min).toBe("0")
84
+ expect(field.max).toBe("255")
85
+ } else {
86
+ throw new Error("Expected number field for nat8")
87
+ }
88
+ })
89
+
90
+ it("should handle nat64 type with min/max", () => {
91
+ const field = visitor.visitFixedNat(
92
+ IDL.Nat64 as IDL.FixedNatClass,
93
+ "timestamp"
94
+ )
95
+
96
+ expect(field.type).toBe("text")
97
+ expect(field.candidType).toBe("nat64")
98
+ // Large numbers are now text fields and don't carry bit/min/max metadata in the same way
99
+ })
100
+
101
+ it("should handle int32 type with min/max", () => {
102
+ const field = visitor.visitFixedInt(
103
+ IDL.Int32 as IDL.FixedIntClass,
104
+ "count"
105
+ )
106
+
107
+ if (field.type === "number") {
108
+ expect(field.type).toBe("number")
109
+ expect(field.candidType).toBe("int32")
110
+ expect(field.bits).toBe(32)
111
+ expect(field.min).toBe("-2147483648")
112
+ expect(field.max).toBe("2147483647")
113
+ } else {
114
+ throw new Error("Expected number field for int32")
115
+ }
116
+ })
117
+
118
+ it("should handle float64 type", () => {
119
+ const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "price")
120
+
121
+ expect(field.type).toBe("number")
122
+ expect(field.candidType).toBe("float64")
123
+ if (field.type === "number") {
124
+ expect(field.isFloat).toBe(true)
125
+ }
126
+ })
127
+ })
128
+
129
+ // ════════════════════════════════════════════════════════════════════════
130
+ // Compound Types
131
+ // ════════════════════════════════════════════════════════════════════════
132
+
133
+ describe("Record Types", () => {
134
+ it("should handle simple record", () => {
135
+ const recordType = IDL.Record({
136
+ name: IDL.Text,
137
+ age: IDL.Nat,
138
+ })
139
+ const field = visitor.visitRecord(
140
+ recordType,
141
+ [
142
+ ["name", IDL.Text],
143
+ ["age", IDL.Nat],
144
+ ],
145
+ "person"
146
+ )
147
+
148
+ expect(field.type).toBe("record")
149
+ expect(field.label).toBe("person")
150
+ expect(field.fields).toHaveLength(2)
151
+ expect(field.fields.some((f) => f.label === "name")).toBe(true)
152
+ expect(field.fields.some((f) => f.label === "age")).toBe(true)
153
+
154
+ const nameField = field.fields.find((f) => f.label === "name")
155
+ if (!nameField || nameField.type !== "text") {
156
+ throw new Error("Name field not found or not text")
157
+ }
158
+ expect(nameField.type).toBe("text")
159
+ expect(nameField.defaultValue).toBe("")
160
+
161
+ const ageField = field.fields.find((f) => f.label === "age")
162
+ if (!ageField || ageField.type !== "text") {
163
+ throw new Error("Age field not found or not text")
164
+ }
165
+ expect(ageField.type).toBe("text")
166
+ expect(ageField.candidType).toBe("nat")
167
+
168
+ expect(field.defaultValue).toEqual({
169
+ name: "",
170
+ age: "",
171
+ })
172
+ })
173
+
174
+ it("should handle nested record", () => {
175
+ const addressType = IDL.Record({
176
+ street: IDL.Text,
177
+ city: IDL.Text,
178
+ })
179
+ const personType = IDL.Record({
180
+ name: IDL.Text,
181
+ address: addressType,
182
+ })
183
+ const field = visitor.visitRecord(
184
+ personType,
185
+ [
186
+ ["name", IDL.Text],
187
+ ["address", addressType],
188
+ ],
189
+ "user"
190
+ )
191
+
192
+ expect(field.type).toBe("record")
193
+ expect(field.fields).toHaveLength(2)
194
+
195
+ const addressField = field.fields.find(
196
+ (f) => f.label === "address"
197
+ ) as RecordField
198
+ if (!addressField || addressField.type !== "record") {
199
+ throw new Error("Address field not found or not record")
200
+ }
201
+ expect(addressField.type).toBe("record")
202
+ expect(addressField.fields).toHaveLength(2)
203
+
204
+ expect(field.defaultValue).toEqual({
205
+ name: "",
206
+ address: {
207
+ street: "",
208
+ city: "",
209
+ },
210
+ })
211
+ })
212
+
213
+ it("should handle ICRC-1 transfer record", () => {
214
+ const transferType = IDL.Record({
215
+ to: IDL.Record({
216
+ owner: IDL.Principal,
217
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
218
+ }),
219
+ amount: IDL.Nat,
220
+ fee: IDL.Opt(IDL.Nat),
221
+ memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
222
+ created_at_time: IDL.Opt(IDL.Nat64),
223
+ })
224
+ const field = visitor.visitRecord(
225
+ transferType,
226
+ [
227
+ [
228
+ "to",
229
+ IDL.Record({
230
+ owner: IDL.Principal,
231
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
232
+ }),
233
+ ],
234
+ ["amount", IDL.Nat],
235
+ ["fee", IDL.Opt(IDL.Nat)],
236
+ ["memo", IDL.Opt(IDL.Vec(IDL.Nat8))],
237
+ ["created_at_time", IDL.Opt(IDL.Nat64)],
238
+ ],
239
+ "transfer"
240
+ )
241
+
242
+ expect(field.type).toBe("record")
243
+ expect(field.fields).toHaveLength(5)
244
+
245
+ // Check 'to' field
246
+ const toField = field.fields.find((f) => f.label === "to") as RecordField
247
+ if (!toField || toField.type !== "record") {
248
+ throw new Error("To field not found or not record")
249
+ }
250
+ expect(toField.type).toBe("record")
251
+ expect(toField.fields).toHaveLength(2)
252
+
253
+ // Check 'amount' field
254
+ const amountField = field.fields.find((f) => f.label === "amount")
255
+ if (!amountField || amountField.type !== "text") {
256
+ throw new Error("Amount field not found or not text")
257
+ }
258
+ expect(amountField.type).toBe("text")
259
+ expect(amountField.candidType).toBe("nat")
260
+
261
+ // Check optional 'fee' field
262
+ const feeField = field.fields.find(
263
+ (f) => f.label === "fee"
264
+ ) as OptionalField
265
+ if (!feeField || feeField.type !== "optional") {
266
+ throw new Error("Fee field not found or not optional")
267
+ }
268
+ expect(feeField.type).toBe("optional")
269
+ expect(feeField.innerField.type).toBe("text")
270
+ })
271
+ })
272
+
273
+ describe("Variant Types", () => {
274
+ it("should handle simple variant", () => {
275
+ const statusType = IDL.Variant({
276
+ Active: IDL.Null,
277
+ Inactive: IDL.Null,
278
+ Pending: IDL.Null,
279
+ })
280
+ const field = visitor.visitVariant(
281
+ statusType,
282
+ [
283
+ ["Inactive", IDL.Null],
284
+ ["Active", IDL.Null],
285
+ ["Pending", IDL.Null],
286
+ ],
287
+ "status"
288
+ )
289
+
290
+ expect(field.type).toBe("variant")
291
+ expect(field.label).toBe("status")
292
+ expect(field.options.map((f) => f.label)).toEqual([
293
+ "Inactive",
294
+ "Active",
295
+ "Pending",
296
+ ])
297
+ expect(field.defaultOption).toBe("Inactive")
298
+ expect(field.options).toHaveLength(3)
299
+ expect(field.options.some((f) => f.label === "Active")).toBe(true)
300
+
301
+ // Test getOptionDefault helper
302
+ expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
303
+ expect(field.getOptionDefault("Pending")).toEqual({ _type: "Pending" })
304
+ })
305
+
306
+ it("should handle variant with payloads", () => {
307
+ const actionType = IDL.Variant({
308
+ Transfer: IDL.Record({
309
+ to: IDL.Principal,
310
+ amount: IDL.Nat,
311
+ }),
312
+ Approve: IDL.Record({
313
+ spender: IDL.Principal,
314
+ amount: IDL.Nat,
315
+ }),
316
+ Burn: IDL.Nat,
317
+ })
318
+ const field = visitor.visitVariant(
319
+ actionType,
320
+ [
321
+ [
322
+ "Approve",
323
+ IDL.Record({
324
+ spender: IDL.Principal,
325
+ amount: IDL.Nat,
326
+ }),
327
+ ],
328
+ ["Burn", IDL.Nat],
329
+ [
330
+ "Transfer",
331
+ IDL.Record({
332
+ to: IDL.Principal,
333
+ amount: IDL.Nat,
334
+ }),
335
+ ],
336
+ ],
337
+ "action"
338
+ )
339
+
340
+ expect(field.type).toBe("variant")
341
+ expect(field.options.map((f) => f.label)).toEqual([
342
+ "Approve",
343
+ "Burn",
344
+ "Transfer",
345
+ ]) // Sorted order
346
+
347
+ const transferField = field.options.find(
348
+ (f) => f.label === "Transfer"
349
+ ) as RecordField
350
+ if (!transferField || transferField.type !== "record") {
351
+ throw new Error("Transfer field not found or not record")
352
+ }
353
+ expect(transferField.type).toBe("record")
354
+ expect(transferField.fields).toHaveLength(2)
355
+
356
+ const burnField = field.options.find((f) => f.label === "Burn")
357
+ if (!burnField || burnField.type !== "text") {
358
+ throw new Error("Burn field not found or not text")
359
+ }
360
+ expect(burnField.type).toBe("text")
361
+ })
362
+
363
+ it("should handle Result variant (Ok/Err)", () => {
364
+ const resultType = IDL.Variant({
365
+ Ok: IDL.Nat,
366
+ Err: IDL.Text,
367
+ })
368
+ const field = visitor.visitVariant(
369
+ resultType,
370
+ [
371
+ ["Ok", IDL.Nat],
372
+ ["Err", IDL.Text],
373
+ ],
374
+ "result"
375
+ )
376
+
377
+ expect(field.type).toBe("variant")
378
+ expect(field.options.some((f) => f.label === "Ok")).toBe(true)
379
+ expect(field.options.some((f) => f.label === "Err")).toBe(true)
380
+
381
+ const okField = field.options.find((f) => f.label === "Ok")
382
+ if (!okField || okField.type !== "text") {
383
+ throw new Error("Ok field not found or not text")
384
+ }
385
+ expect(okField.type).toBe("text")
386
+
387
+ const errField = field.options.find((f) => f.label === "Err")
388
+ if (!errField || errField.type !== "text") {
389
+ throw new Error("Err field not found or not text")
390
+ }
391
+ expect(errField.type).toBe("text")
392
+ })
393
+ })
394
+
395
+ describe("Tuple Types", () => {
396
+ it("should handle simple tuple", () => {
397
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
398
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
399
+
400
+ expect(field.type).toBe("tuple")
401
+ expect(field.label).toBe("pair")
402
+ expect(field.fields).toHaveLength(2)
403
+ expect(field.fields[0].type).toBe("text")
404
+ expect(field.fields[1].type).toBe("text")
405
+ expect(field.defaultValue).toEqual(["", ""])
406
+ })
407
+
408
+ it("triple tuple", () => {
409
+ const tupleType = IDL.Tuple(IDL.Principal, IDL.Nat, IDL.Bool)
410
+ const field = visitor.visitTuple(
411
+ tupleType,
412
+ [IDL.Principal, IDL.Nat, IDL.Bool],
413
+ "triple"
414
+ )
415
+
416
+ expect(field.type).toBe("tuple")
417
+ expect(field.fields).toHaveLength(3)
418
+ expect(field.fields[0].type).toBe("principal")
419
+ expect(field.fields[1].type).toBe("text")
420
+ expect(field.fields[2].type).toBe("boolean")
421
+ expect(field.defaultValue).toEqual(["", "", false])
422
+ })
423
+ })
424
+
425
+ describe("Optional Types", () => {
426
+ it("should handle optional primitive", () => {
427
+ const optType = IDL.Opt(IDL.Text)
428
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
429
+
430
+ expect(field.type).toBe("optional")
431
+ expect(field.label).toBe("nickname")
432
+ expect(field.defaultValue).toBe(null)
433
+ expect(field.innerField.type).toBe("text")
434
+ expect(field.getInnerDefault()).toBe("")
435
+ })
436
+
437
+ it("should handle optional record", () => {
438
+ const recType = IDL.Record({
439
+ name: IDL.Text,
440
+ value: IDL.Nat,
441
+ })
442
+ const optType = IDL.Opt(recType)
443
+ const field = visitor.visitOpt(optType, recType, "metadata")
444
+
445
+ expect(field.type).toBe("optional")
446
+ expect(field.innerField.type).toBe("record")
447
+ expect(field.innerField.type).toBe("record")
448
+ const inner = field.innerField as RecordField
449
+ if (inner.type === "record") {
450
+ expect(inner.fields).toHaveLength(2)
451
+ } else {
452
+ throw new Error("Inner field is not record")
453
+ }
454
+
455
+ // Test getInnerDefault helper
456
+ expect(field.getInnerDefault()).toEqual({ name: "", value: "" })
457
+ })
458
+
459
+ it("should handle nested optional", () => {
460
+ const innerOpt = IDL.Opt(IDL.Nat)
461
+ const optType = IDL.Opt(innerOpt)
462
+ const field = visitor.visitOpt(optType, innerOpt, "maybeNumber")
463
+
464
+ expect(field.type).toBe("optional")
465
+ expect(field.innerField.type).toBe("optional")
466
+ expect(field.innerField.type).toBe("optional")
467
+ const inner = field.innerField as OptionalField
468
+ if (inner.type === "optional") {
469
+ expect(inner.innerField.type).toBe("text")
470
+ } else {
471
+ throw new Error("Inner field is not optional")
472
+ }
473
+ })
474
+ })
475
+
476
+ describe("Vector Types", () => {
477
+ it("should handle vector of primitives", () => {
478
+ const vecType = IDL.Vec(IDL.Text)
479
+ const field = visitor.visitVec(vecType, IDL.Text, "tags") as VectorField
480
+
481
+ expect(field.type).toBe("vector")
482
+ expect(field.label).toBe("tags")
483
+ expect(field.defaultValue).toEqual([])
484
+ expect(field.itemField.type).toBe("text")
485
+ expect(field.getItemDefault()).toBe("")
486
+ })
487
+
488
+ it("should handle vector of records", () => {
489
+ const recType = IDL.Record({
490
+ id: IDL.Nat,
491
+ name: IDL.Text,
492
+ })
493
+ const vecType = IDL.Vec(recType)
494
+ const field = visitor.visitVec(vecType, recType, "items") as VectorField
495
+
496
+ expect(field.type).toBe("vector")
497
+ expect(field.itemField.type).toBe("record")
498
+ const item = field.itemField as RecordField
499
+ if (item.type === "record") {
500
+ expect(item.fields).toHaveLength(2)
501
+ } else {
502
+ throw new Error("Item field is not record")
503
+ }
504
+
505
+ // Test getItemDefault helper
506
+ expect(field.getItemDefault()).toEqual({ id: "", name: "" })
507
+ })
508
+
509
+ it("blob (vec nat8)", () => {
510
+ const blobType = IDL.Vec(IDL.Nat8)
511
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
512
+
513
+ expect(field.type).toBe("blob")
514
+ expect(field.label).toBe("data")
515
+ expect(field.defaultValue).toBe("")
516
+ if (field.type === "blob") {
517
+ expect(field.acceptedFormats).toEqual(["hex", "base64", "file"])
518
+ }
519
+ })
520
+
521
+ it("should handle nested vectors", () => {
522
+ const innerVec = IDL.Vec(IDL.Nat)
523
+ const nestedVecType = IDL.Vec(innerVec)
524
+ const field = visitor.visitVec(nestedVecType, innerVec, "matrix")
525
+
526
+ expect(field.type).toBe("vector")
527
+ expect(field.itemField.type).toBe("vector")
528
+ const item = field.itemField as VectorField
529
+ if (item.type === "vector") {
530
+ expect(item.itemField.type).toBe("text")
531
+ } else {
532
+ throw new Error("Item field is not vector")
533
+ }
534
+ })
535
+ })
536
+
537
+ describe("Recursive Types", () => {
538
+ it("should handle recursive type (tree)", () => {
539
+ const Tree = IDL.Rec()
540
+ Tree.fill(
541
+ IDL.Variant({
542
+ Leaf: IDL.Nat,
543
+ Node: IDL.Record({
544
+ left: Tree,
545
+ right: Tree,
546
+ }),
547
+ })
548
+ )
549
+
550
+ const field = visitor.visitRec(
551
+ Tree,
552
+ IDL.Variant({
553
+ Leaf: IDL.Nat,
554
+ Node: IDL.Record({
555
+ left: Tree,
556
+ right: Tree,
557
+ }),
558
+ }),
559
+ "tree"
560
+ )
561
+
562
+ expect(field.type).toBe("recursive")
563
+ expect(field.label).toBe("tree")
564
+ expect(field.typeName).toBeDefined()
565
+ expect(typeof field.extract).toBe("function")
566
+ expect(typeof field.getInnerDefault).toBe("function")
567
+
568
+ // Extract should return a variant
569
+ const extracted = field.extract() as VariantField
570
+ if (extracted.type !== "variant") {
571
+ throw new Error("Extracted field is not variant")
572
+ }
573
+ expect(extracted.type).toBe("variant")
574
+ expect(extracted.options.some((f) => f.label === "Leaf")).toBe(true)
575
+ expect(extracted.options.some((f) => f.label === "Node")).toBe(true)
576
+ })
577
+
578
+ it("should handle recursive linked list", () => {
579
+ const List = IDL.Rec()
580
+ List.fill(
581
+ IDL.Variant({
582
+ Nil: IDL.Null,
583
+ Cons: IDL.Record({
584
+ head: IDL.Nat,
585
+ tail: List,
586
+ }),
587
+ })
588
+ )
589
+
590
+ const field = visitor.visitRec(
591
+ List,
592
+ IDL.Variant({
593
+ Nil: IDL.Null,
594
+ Cons: IDL.Record({
595
+ head: IDL.Nat,
596
+ tail: List,
597
+ }),
598
+ }),
599
+ "list"
600
+ )
601
+
602
+ expect(field.type).toBe("recursive")
603
+
604
+ const extracted = field.extract() as VariantField
605
+ if (extracted.type !== "variant") {
606
+ throw new Error("Extracted field is not variant")
607
+ }
608
+ expect(extracted.type).toBe("variant")
609
+ expect(extracted.options.map((f) => f.label)).toEqual(["Nil", "Cons"])
610
+
611
+ const consField = extracted.options.find(
612
+ (f) => f.label === "Cons"
613
+ ) as RecordField
614
+ if (!consField || consField.type !== "record") {
615
+ throw new Error("Cons field not found or not record")
616
+ }
617
+ expect(consField.type).toBe("record")
618
+ expect(consField.fields).toHaveLength(2)
619
+ })
620
+ })
621
+
622
+ // ════════════════════════════════════════════════════════════════════════
623
+ // Function Types
624
+ // ════════════════════════════════════════════════════════════════════════
625
+
626
+ describe("Function Types", () => {
627
+ it("should handle query function", () => {
628
+ const funcType = IDL.Func([IDL.Text], [IDL.Opt(IDL.Text)], ["query"])
629
+ const meta = visitor.visitFunc(funcType, "lookup")
630
+
631
+ expect(meta.functionType).toBe("query")
632
+ expect(meta.functionName).toBe("lookup")
633
+ expect(meta.args).toHaveLength(1)
634
+ expect(meta.args[0].type).toBe("text")
635
+ expect(meta.defaults).toEqual([""])
636
+ expect(meta.argCount).toBe(1)
637
+ expect(meta.isEmpty).toBe(false)
638
+ })
639
+
640
+ it("should handle update function", () => {
641
+ const funcType = IDL.Func(
642
+ [
643
+ IDL.Record({
644
+ to: IDL.Principal,
645
+ amount: IDL.Nat,
646
+ }),
647
+ ],
648
+ [
649
+ IDL.Variant({
650
+ Ok: IDL.Nat,
651
+ Err: IDL.Text,
652
+ }),
653
+ ],
654
+ []
655
+ )
656
+ const meta = visitor.visitFunc(funcType, "transfer")
657
+
658
+ expect(meta.functionType).toBe("update")
659
+ expect(meta.functionName).toBe("transfer")
660
+ expect(meta.args).toHaveLength(1)
661
+ expect(meta.args[0].type).toBe("record")
662
+
663
+ const recordField = meta.args[0] as RecordField
664
+ if (recordField.type !== "record") {
665
+ throw new Error("Expected record field")
666
+ }
667
+ expect(recordField.fields).toHaveLength(2)
668
+ })
669
+
670
+ it("should handle function with multiple arguments", () => {
671
+ const funcType = IDL.Func(
672
+ [IDL.Principal, IDL.Nat, IDL.Opt(IDL.Vec(IDL.Nat8))],
673
+ [IDL.Bool],
674
+ []
675
+ )
676
+ const meta = visitor.visitFunc(funcType, "authorize")
677
+
678
+ expect(meta.args).toHaveLength(3)
679
+ expect(meta.args[0].type).toBe("principal")
680
+ expect(meta.args[1].type).toBe("text")
681
+ expect(meta.args[2].type).toBe("optional")
682
+ expect(meta.argCount).toBe(3)
683
+ })
684
+
685
+ it("should handle function with no arguments", () => {
686
+ const funcType = IDL.Func([], [IDL.Nat], ["query"])
687
+ const meta = visitor.visitFunc(funcType, "getBalance")
688
+
689
+ expect(meta.functionType).toBe("query")
690
+ expect(meta.args).toHaveLength(0)
691
+ expect(meta.defaults).toEqual([])
692
+ expect(meta.argCount).toBe(0)
693
+ expect(meta.isEmpty).toBe(true)
694
+ })
695
+ })
696
+
697
+ // ════════════════════════════════════════════════════════════════════════
698
+ // Service Types
699
+ // ════════════════════════════════════════════════════════════════════════
700
+
701
+ describe("Service Types", () => {
702
+ it("should handle complete service", () => {
703
+ const serviceType = IDL.Service({
704
+ get_balance: IDL.Func([IDL.Principal], [IDL.Nat], ["query"]),
705
+ transfer: IDL.Func(
706
+ [
707
+ IDL.Record({
708
+ to: IDL.Principal,
709
+ amount: IDL.Nat,
710
+ }),
711
+ ],
712
+ [
713
+ IDL.Variant({
714
+ Ok: IDL.Nat,
715
+ Err: IDL.Text,
716
+ }),
717
+ ],
718
+ []
719
+ ),
720
+ get_metadata: IDL.Func(
721
+ [],
722
+ [IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))],
723
+ ["query"]
724
+ ),
725
+ })
726
+
727
+ const serviceMeta = visitor.visitService(serviceType)
728
+
729
+ expect(Object.keys(serviceMeta)).toHaveLength(3)
730
+ expect(serviceMeta).toHaveProperty("get_balance")
731
+ expect(serviceMeta).toHaveProperty("transfer")
732
+ expect(serviceMeta).toHaveProperty("get_metadata")
733
+
734
+ // Check get_balance
735
+ const getBalanceMeta = serviceMeta["get_balance"]
736
+ expect(getBalanceMeta.functionType).toBe("query")
737
+ expect(getBalanceMeta.args).toHaveLength(1)
738
+ expect(getBalanceMeta.args[0].type).toBe("principal")
739
+
740
+ // Check transfer
741
+ const transferMeta = serviceMeta["transfer"]
742
+ expect(transferMeta.functionType).toBe("update")
743
+ expect(transferMeta.args).toHaveLength(1)
744
+ expect(transferMeta.args[0].type).toBe("record")
745
+
746
+ // Check get_metadata
747
+ const getMetadataMeta = serviceMeta["get_metadata"]
748
+ expect(getMetadataMeta.functionType).toBe("query")
749
+ expect(getMetadataMeta.args).toHaveLength(0)
750
+ expect(getMetadataMeta.isEmpty).toBe(true)
751
+ })
752
+ })
753
+
754
+ // ════════════════════════════════════════════════════════════════════════
755
+ // Name (Path) Generation
756
+ // ════════════════════════════════════════════════════════════════════════
757
+
758
+ describe("Name Generation", () => {
759
+ it("should generate correct names for nested records", () => {
760
+ const funcType = IDL.Func(
761
+ [
762
+ IDL.Record({
763
+ user: IDL.Record({
764
+ name: IDL.Text,
765
+ age: IDL.Nat,
766
+ }),
767
+ active: IDL.Bool,
768
+ }),
769
+ ],
770
+ [],
771
+ []
772
+ )
773
+ const meta = visitor.visitFunc(funcType, "updateUser")
774
+
775
+ const argRecord = meta.args[0] as RecordField
776
+ if (argRecord.type !== "record") {
777
+ throw new Error("Expected record field")
778
+ }
779
+ expect(argRecord.name).toBe("[0]")
780
+
781
+ const userRecord = argRecord.fields.find(
782
+ (f) => f.label === "user"
783
+ ) as RecordField
784
+ if (!userRecord || userRecord.type !== "record") {
785
+ throw new Error("User record not found or not record")
786
+ }
787
+ expect(userRecord.name).toBe("[0].user")
788
+
789
+ const nameField = userRecord.fields.find((f) => f.label === "name")
790
+ if (!nameField || nameField.type !== "text") {
791
+ throw new Error("Name field not found or not text")
792
+ }
793
+ expect(nameField.name).toBe("[0].user.name")
794
+
795
+ const ageField = userRecord.fields.find((f) => f.label === "age")
796
+ if (!ageField || ageField.type !== "text") {
797
+ throw new Error("Age field not found or not text")
798
+ }
799
+ expect(ageField.name).toBe("[0].user.age")
800
+ })
801
+
802
+ it("should generate correct names for vectors", () => {
803
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
804
+ const meta = visitor.visitFunc(funcType, "addTags")
805
+
806
+ const vecField = meta.args[0] as VectorField
807
+ if (vecField.type !== "vector") {
808
+ throw new Error("Expected vector field")
809
+ }
810
+ expect(vecField.name).toBe("[0]")
811
+ expect(vecField.itemField.name).toBe("[0][0]")
812
+ })
813
+ })
814
+
815
+ // ════════════════════════════════════════════════════════════════════════
816
+ // Complex Real-World Examples
817
+ // ════════════════════════════════════════════════════════════════════════
818
+
819
+ describe("Real-World Examples", () => {
820
+ it("should handle ICRC-2 approve", () => {
821
+ const ApproveArgs = IDL.Record({
822
+ fee: IDL.Opt(IDL.Nat),
823
+ memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
824
+ from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
825
+ created_at_time: IDL.Opt(IDL.Nat64),
826
+ amount: IDL.Nat,
827
+ expected_allowance: IDL.Opt(IDL.Nat),
828
+ expires_at: IDL.Opt(IDL.Nat64),
829
+ spender: IDL.Record({
830
+ owner: IDL.Principal,
831
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
832
+ }),
833
+ })
834
+
835
+ const field = visitor.visitRecord(
836
+ ApproveArgs,
837
+ [
838
+ ["fee", IDL.Opt(IDL.Nat)],
839
+ ["memo", IDL.Opt(IDL.Vec(IDL.Nat8))],
840
+ ["from_subaccount", IDL.Opt(IDL.Vec(IDL.Nat8))],
841
+ ["created_at_time", IDL.Opt(IDL.Nat64)],
842
+ ["amount", IDL.Nat],
843
+ ["expected_allowance", IDL.Opt(IDL.Nat)],
844
+ ["expires_at", IDL.Opt(IDL.Nat64)],
845
+ [
846
+ "spender",
847
+ IDL.Record({
848
+ owner: IDL.Principal,
849
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
850
+ }),
851
+ ],
852
+ ],
853
+ "approve"
854
+ )
855
+
856
+ expect(field.type).toBe("record")
857
+ expect(field.fields.length).toBeGreaterThan(5)
858
+
859
+ // Check spender field
860
+ const spenderField = field.fields.find(
861
+ (f) => f.label === "spender"
862
+ ) as RecordField
863
+ if (!spenderField || spenderField.type !== "record") {
864
+ throw new Error("Spender field not found or not record")
865
+ }
866
+ expect(spenderField.type).toBe("record")
867
+ expect(spenderField.fields).toHaveLength(2)
868
+
869
+ // Check amount field
870
+ const amountField = field.fields.find((f) => f.label === "amount")
871
+ if (!amountField || amountField.type !== "text") {
872
+ throw new Error("Amount field not found or not text")
873
+ }
874
+ expect(amountField.type).toBe("text")
875
+ expect(amountField.candidType).toBe("nat")
876
+ })
877
+
878
+ it("should handle SNS governance proposal", () => {
879
+ const ProposalType = IDL.Variant({
880
+ Motion: IDL.Record({
881
+ motion_text: IDL.Text,
882
+ }),
883
+ TransferSnsTreasuryFunds: IDL.Record({
884
+ from_treasury: IDL.Nat32,
885
+ to_principal: IDL.Opt(IDL.Principal),
886
+ to_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
887
+ memo: IDL.Opt(IDL.Nat64),
888
+ amount_e8s: IDL.Nat64,
889
+ }),
890
+ UpgradeSnsControlledCanister: IDL.Record({
891
+ new_canister_wasm: IDL.Vec(IDL.Nat8),
892
+ mode: IDL.Opt(IDL.Nat32),
893
+ canister_id: IDL.Opt(IDL.Principal),
894
+ canister_upgrade_arg: IDL.Opt(IDL.Vec(IDL.Nat8)),
895
+ }),
896
+ })
897
+
898
+ const field = visitor.visitVariant(
899
+ ProposalType,
900
+ [
901
+ ["Motion", IDL.Record({ motion_text: IDL.Text })],
902
+ [
903
+ "TransferSnsTreasuryFunds",
904
+ IDL.Record({
905
+ from_treasury: IDL.Nat32,
906
+ to_principal: IDL.Opt(IDL.Principal),
907
+ to_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
908
+ memo: IDL.Opt(IDL.Nat64),
909
+ amount_e8s: IDL.Nat64,
910
+ }),
911
+ ],
912
+ [
913
+ "UpgradeSnsControlledCanister",
914
+ IDL.Record({
915
+ new_canister_wasm: IDL.Vec(IDL.Nat8),
916
+ mode: IDL.Opt(IDL.Nat32),
917
+ canister_id: IDL.Opt(IDL.Principal),
918
+ canister_upgrade_arg: IDL.Opt(IDL.Vec(IDL.Nat8)),
919
+ }),
920
+ ],
921
+ ],
922
+ "proposal"
923
+ )
924
+
925
+ expect(field.type).toBe("variant")
926
+ expect(field.options.some((f) => f.label === "Motion")).toBe(true)
927
+ expect(
928
+ field.options.some((f) => f.label === "TransferSnsTreasuryFunds")
929
+ ).toBe(true)
930
+ expect(
931
+ field.options.some((f) => f.label === "UpgradeSnsControlledCanister")
932
+ ).toBe(true)
933
+
934
+ // Check Motion variant
935
+ const motionField = field.options.find(
936
+ (f) => f.label === "Motion"
937
+ ) as RecordField
938
+ if (!motionField || motionField.type !== "record") {
939
+ throw new Error("Motion field not found or not record")
940
+ }
941
+ expect(motionField.type).toBe("record")
942
+ expect(motionField.fields).toHaveLength(1)
943
+
944
+ // Check TransferSnsTreasuryFunds variant
945
+ const transferField = field.options.find(
946
+ (f) => f.label === "TransferSnsTreasuryFunds"
947
+ ) as RecordField
948
+ if (!transferField || transferField.type !== "record") {
949
+ throw new Error("Transfer field not found or not record")
950
+ }
951
+ expect(transferField.type).toBe("record")
952
+ expect(transferField.fields.length).toBeGreaterThan(3)
953
+ })
954
+ })
955
+
956
+ // ════════════════════════════════════════════════════════════════════════
957
+ // Helper Methods
958
+ // ════════════════════════════════════════════════════════════════════════
959
+
960
+ describe("Helper Methods", () => {
961
+ it("variant getOptionDefault should return correct defaults", () => {
962
+ const statusType = IDL.Variant({
963
+ Active: IDL.Null,
964
+ Pending: IDL.Record({ reason: IDL.Text }),
965
+ })
966
+ const field = visitor.visitVariant(
967
+ statusType,
968
+ [
969
+ ["Active", IDL.Null],
970
+ ["Pending", IDL.Record({ reason: IDL.Text })],
971
+ ],
972
+ "status"
973
+ )
974
+
975
+ expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
976
+ expect(field.getOptionDefault("Pending")).toEqual({
977
+ _type: "Pending",
978
+ Pending: { reason: "" },
979
+ })
980
+ })
981
+
982
+ it("vector getItemDefault should return item default", () => {
983
+ const vecType = IDL.Vec(IDL.Record({ name: IDL.Text }))
984
+ const field = visitor.visitVec(
985
+ vecType,
986
+ IDL.Record({ name: IDL.Text }),
987
+ "items"
988
+ ) as VectorField
989
+
990
+ expect(field.getItemDefault()).toEqual({ name: "" })
991
+ })
992
+
993
+ it("optional getInnerDefault should return inner default", () => {
994
+ const optType = IDL.Opt(IDL.Record({ value: IDL.Nat }))
995
+ const field = visitor.visitOpt(
996
+ optType,
997
+ IDL.Record({ value: IDL.Nat }),
998
+ "config"
999
+ )
1000
+
1001
+ expect(field.getInnerDefault()).toEqual({ value: "" })
1002
+ })
1003
+ })
1004
+
1005
+ // ════════════════════════════════════════════════════════════════════════
1006
+ // New Features - displayLabel, component, renderHint
1007
+ // ════════════════════════════════════════════════════════════════════════
1008
+
1009
+ describe("displayLabel formatting", () => {
1010
+ it("should format __arg labels correctly", () => {
1011
+ const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
1012
+ const meta = visitor.visitFunc(funcType, "test")
1013
+
1014
+ expect(meta.args[0].displayLabel).toBe("Arg 0")
1015
+ expect(meta.args[1].displayLabel).toBe("Arg 1")
1016
+ })
1017
+
1018
+ it("should format tuple index labels correctly", () => {
1019
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat, IDL.Bool)
1020
+ const field = visitor.visitTuple(
1021
+ tupleType,
1022
+ [IDL.Text, IDL.Nat, IDL.Bool],
1023
+ "triple"
1024
+ )
1025
+
1026
+ expect(field.fields[0].displayLabel).toBe("Item 0")
1027
+ expect(field.fields[1].displayLabel).toBe("Item 1")
1028
+ expect(field.fields[2].displayLabel).toBe("Item 2")
1029
+ })
1030
+
1031
+ it("should format snake_case labels correctly", () => {
1032
+ const recordType = IDL.Record({
1033
+ created_at_time: IDL.Nat,
1034
+ user_address: IDL.Text,
1035
+ })
1036
+ const field = visitor.visitRecord(
1037
+ recordType,
1038
+ [
1039
+ ["created_at_time", IDL.Nat],
1040
+ ["user_address", IDL.Text],
1041
+ ],
1042
+ "record"
1043
+ )
1044
+
1045
+ const createdField = field.fields.find(
1046
+ (f) => f.label === "created_at_time"
1047
+ )
1048
+ const userField = field.fields.find((f) => f.label === "user_address")
1049
+
1050
+ expect(createdField?.displayLabel).toBe("Created At Time")
1051
+ expect(userField?.displayLabel).toBe("User Address")
1052
+ })
1053
+ })
1054
+
1055
+ describe("component hints", () => {
1056
+ it("should have correct component for record", () => {
1057
+ const recordType = IDL.Record({ name: IDL.Text })
1058
+ const field = visitor.visitRecord(
1059
+ recordType,
1060
+ [["name", IDL.Text]],
1061
+ "person"
1062
+ )
1063
+
1064
+ expect(field.component).toBe("record-container")
1065
+ })
1066
+
1067
+ it("should have correct component for variant", () => {
1068
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text })
1069
+ const field = visitor.visitVariant(
1070
+ variantType,
1071
+ [
1072
+ ["A", IDL.Null],
1073
+ ["B", IDL.Text],
1074
+ ],
1075
+ "choice"
1076
+ )
1077
+
1078
+ expect(field.component).toBe("variant-select")
1079
+ })
1080
+
1081
+ it("should have correct component for optional", () => {
1082
+ const optType = IDL.Opt(IDL.Text)
1083
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1084
+
1085
+ expect(field.component).toBe("optional-toggle")
1086
+ })
1087
+
1088
+ it("should have correct component for vector", () => {
1089
+ const vecType = IDL.Vec(IDL.Text)
1090
+ const field = visitor.visitVec(vecType, IDL.Text, "vec") as VectorField
1091
+
1092
+ expect(field.component).toBe("vector-list")
1093
+ })
1094
+
1095
+ it("should have correct component for blob", () => {
1096
+ const blobType = IDL.Vec(IDL.Nat8)
1097
+ const field = visitor.visitVec(blobType, IDL.Nat8, "blob")
1098
+
1099
+ expect(field.component).toBe("blob-upload")
1100
+ })
1101
+
1102
+ it("should have correct component for text", () => {
1103
+ const field = visitor.visitText(IDL.Text, "text")
1104
+
1105
+ expect(field.component).toBe("text-input")
1106
+ })
1107
+
1108
+ it("should have correct component for number", () => {
1109
+ const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "num")
1110
+
1111
+ expect(field.component).toBe("number-input")
1112
+ })
1113
+
1114
+ it("should have correct component for boolean", () => {
1115
+ const field = visitor.visitBool(IDL.Bool, "bool")
1116
+
1117
+ expect(field.component).toBe("boolean-checkbox")
1118
+ })
1119
+
1120
+ it("should have correct component for principal", () => {
1121
+ const field = visitor.visitPrincipal(IDL.Principal, "principal")
1122
+
1123
+ expect(field.component).toBe("principal-input")
1124
+ })
1125
+
1126
+ it("should have correct component for null", () => {
1127
+ const field = visitor.visitNull(IDL.Null, "null")
1128
+
1129
+ expect(field.component).toBe("null-hidden")
1130
+ })
1131
+ })
1132
+
1133
+ describe("renderHint properties", () => {
1134
+ it("compound types should have isCompound: true", () => {
1135
+ const recordField = visitor.visitRecord(
1136
+ IDL.Record({ x: IDL.Text }),
1137
+ [["x", IDL.Text]],
1138
+ "rec"
1139
+ )
1140
+ const variantField = visitor.visitVariant(
1141
+ IDL.Variant({ A: IDL.Null }),
1142
+ [["A", IDL.Null]],
1143
+ "var"
1144
+ )
1145
+ const optionalField = visitor.visitOpt(IDL.Opt(IDL.Text), IDL.Text, "opt")
1146
+ const vectorField = visitor.visitVec(IDL.Vec(IDL.Text), IDL.Text, "vec")
1147
+
1148
+ expect(recordField.renderHint.isCompound).toBe(true)
1149
+ expect(recordField.renderHint.isPrimitive).toBe(false)
1150
+
1151
+ expect(variantField.renderHint.isCompound).toBe(true)
1152
+ expect(optionalField.renderHint.isCompound).toBe(true)
1153
+ expect((vectorField as VectorField).renderHint.isCompound).toBe(true)
1154
+ })
1155
+
1156
+ it("primitive types should have isPrimitive: true", () => {
1157
+ const textField = visitor.visitText(IDL.Text, "text")
1158
+ const boolField = visitor.visitBool(IDL.Bool, "bool")
1159
+ const principalField = visitor.visitPrincipal(IDL.Principal, "principal")
1160
+
1161
+ expect(textField.renderHint.isPrimitive).toBe(true)
1162
+ expect(textField.renderHint.isCompound).toBe(false)
1163
+
1164
+ expect(boolField.renderHint.isPrimitive).toBe(true)
1165
+ expect(principalField.renderHint.isPrimitive).toBe(true)
1166
+ })
1167
+
1168
+ it("should have correct inputType hints", () => {
1169
+ const textField = visitor.visitText(IDL.Text, "text")
1170
+ const boolField = visitor.visitBool(IDL.Bool, "bool")
1171
+ const numField = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "num")
1172
+
1173
+ expect(textField.renderHint.inputType).toBe("text")
1174
+ expect(boolField.renderHint.inputType).toBe("checkbox")
1175
+ expect(numField.renderHint.inputType).toBe("number")
1176
+ })
1177
+ })
1178
+
1179
+ describe("inputProps for primitive types", () => {
1180
+ it("text field should have inputProps", () => {
1181
+ const field = visitor.visitText(IDL.Text, "text")
1182
+
1183
+ expect(field.inputProps).toBeDefined()
1184
+ expect(field.inputProps.type).toBe("text")
1185
+ expect(field.inputProps.placeholder).toBeDefined()
1186
+ })
1187
+
1188
+ it("boolean field should have checkbox inputProps", () => {
1189
+ const field = visitor.visitBool(IDL.Bool, "bool")
1190
+
1191
+ expect(field.inputProps).toBeDefined()
1192
+ expect(field.inputProps.type).toBe("checkbox")
1193
+ })
1194
+
1195
+ it("number field should have number inputProps with min/max", () => {
1196
+ const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
1197
+
1198
+ if (field.type === "number") {
1199
+ expect(field.inputProps).toBeDefined()
1200
+ expect(field.inputProps.type).toBe("number")
1201
+ expect(field.inputProps.min).toBe("0")
1202
+ expect(field.inputProps.max).toBe("255")
1203
+ }
1204
+ })
1205
+
1206
+ it("principal field should have inputProps with spellCheck disabled", () => {
1207
+ const field = visitor.visitPrincipal(IDL.Principal, "principal")
1208
+
1209
+ expect(field.inputProps).toBeDefined()
1210
+ expect(field.inputProps.spellCheck).toBe(false)
1211
+ expect(field.inputProps.autoComplete).toBe("off")
1212
+ })
1213
+ })
1214
+
1215
+ // ════════════════════════════════════════════════════════════════════════
1216
+ // Enhanced Variant Helpers
1217
+ // ════════════════════════════════════════════════════════════════════════
1218
+
1219
+ describe("Variant helper methods", () => {
1220
+ it("getField should return the correct field for an option", () => {
1221
+ const variantType = IDL.Variant({
1222
+ Transfer: IDL.Record({ to: IDL.Principal, amount: IDL.Nat }),
1223
+ Burn: IDL.Nat,
1224
+ })
1225
+ const field = visitor.visitVariant(
1226
+ variantType,
1227
+ [
1228
+ ["Transfer", IDL.Record({ to: IDL.Principal, amount: IDL.Nat })],
1229
+ ["Burn", IDL.Nat],
1230
+ ],
1231
+ "action"
1232
+ )
1233
+
1234
+ const transferField = field.getOption("Transfer")
1235
+ expect(transferField.type).toBe("record")
1236
+
1237
+ const burnField = field.getOption("Burn")
1238
+ expect(burnField.type).toBe("text") // nat is rendered as text for large numbers
1239
+ })
1240
+
1241
+ it("getSelectedKey should return the selected key from a value", () => {
1242
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text, C: IDL.Nat })
1243
+ const field = visitor.visitVariant(
1244
+ variantType,
1245
+ [
1246
+ ["A", IDL.Null],
1247
+ ["B", IDL.Text],
1248
+ ["C", IDL.Nat],
1249
+ ],
1250
+ "choice"
1251
+ )
1252
+
1253
+ expect(field.getSelectedKey({ A: null })).toBe("A")
1254
+ expect(field.getSelectedKey({ B: "hello" })).toBe("B")
1255
+ expect(field.getSelectedKey({ C: "100" })).toBe("C")
1256
+ // Falls back to default option for unknown values
1257
+ expect(field.getSelectedKey({})).toBe("A")
1258
+ })
1259
+
1260
+ it("getSelectedField should return the field for the selected option", () => {
1261
+ const variantType = IDL.Variant({
1262
+ Ok: IDL.Nat,
1263
+ Err: IDL.Text,
1264
+ })
1265
+ const field = visitor.visitVariant(
1266
+ variantType,
1267
+ [
1268
+ ["Ok", IDL.Nat],
1269
+ ["Err", IDL.Text],
1270
+ ],
1271
+ "result"
1272
+ )
1273
+
1274
+ const okField = field.getSelectedOption({ Ok: "100" })
1275
+ expect(okField.label).toBe("Ok")
1276
+
1277
+ const errField = field.getSelectedOption({ Err: "error" })
1278
+ expect(errField.label).toBe("Err")
1279
+ })
1280
+ })
1281
+
1282
+ // ════════════════════════════════════════════════════════════════════════
1283
+ // Optional isEnabled Helper
1284
+ // ════════════════════════════════════════════════════════════════════════
1285
+
1286
+ describe("Optional isEnabled helper", () => {
1287
+ it("should return true for non-null values", () => {
1288
+ const optType = IDL.Opt(IDL.Text)
1289
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1290
+
1291
+ expect(field.isEnabled("hello")).toBe(true)
1292
+ expect(field.isEnabled("")).toBe(true)
1293
+ expect(field.isEnabled(0)).toBe(true)
1294
+ expect(field.isEnabled(false)).toBe(true)
1295
+ expect(field.isEnabled({})).toBe(true)
1296
+ })
1297
+
1298
+ it("should return false for null and undefined", () => {
1299
+ const optType = IDL.Opt(IDL.Text)
1300
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1301
+
1302
+ expect(field.isEnabled(null)).toBe(false)
1303
+ expect(field.isEnabled(undefined)).toBe(false)
1304
+ })
1305
+ })
1306
+
1307
+ // ════════════════════════════════════════════════════════════════════════
1308
+ // Vector createItemField Helper
1309
+ // ════════════════════════════════════════════════════════════════════════
1310
+
1311
+ describe("Vector createItemField helper", () => {
1312
+ it("should create item field with correct index in name path", () => {
1313
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
1314
+ const meta = visitor.visitFunc(funcType, "addItems")
1315
+ const vecField = meta.args[0] as VectorField
1316
+
1317
+ const item0 = vecField.createItemField(0)
1318
+ expect(item0.name).toBe("[0][0]")
1319
+
1320
+ const item5 = vecField.createItemField(5)
1321
+ expect(item5.name).toBe("[0][5]")
1322
+ })
1323
+
1324
+ it("should use custom label when provided", () => {
1325
+ const funcType = IDL.Func(
1326
+ [IDL.Vec(IDL.Record({ name: IDL.Text }))],
1327
+ [],
1328
+ []
1329
+ )
1330
+ const meta = visitor.visitFunc(funcType, "addItems")
1331
+ const vecField = meta.args[0] as VectorField
1332
+
1333
+ const item = vecField.createItemField(3, { label: "Person 3" })
1334
+ expect(item.label).toBe("Person 3")
1335
+ expect(item.displayLabel).toBe("Person 3")
1336
+ })
1337
+
1338
+ it("should use default label when not provided", () => {
1339
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
1340
+ const meta = visitor.visitFunc(funcType, "addTags")
1341
+ const vecField = meta.args[0] as VectorField
1342
+
1343
+ const item = vecField.createItemField(2)
1344
+ expect(item.label).toBe("Item 2")
1345
+ expect(item.displayLabel).toBe("Item 2")
1346
+ })
1347
+ })
1348
+
1349
+ // ════════════════════════════════════════════════════════════════════════
1350
+ // Blob Field Utilities
1351
+ // ════════════════════════════════════════════════════════════════════════
1352
+
1353
+ describe("Blob field utilities", () => {
1354
+ it("should have limits defined", () => {
1355
+ const blobType = IDL.Vec(IDL.Nat8)
1356
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1357
+
1358
+ if (field.type === "blob") {
1359
+ expect(field.limits).toBeDefined()
1360
+ expect(field.limits.maxHexBytes).toBeGreaterThan(0)
1361
+ expect(field.limits.maxFileBytes).toBeGreaterThan(0)
1362
+ expect(field.limits.maxHexDisplayLength).toBeGreaterThan(0)
1363
+ }
1364
+ })
1365
+
1366
+ it("normalizeHex should remove 0x prefix and lowercase", () => {
1367
+ const blobType = IDL.Vec(IDL.Nat8)
1368
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1369
+
1370
+ if (field.type === "blob") {
1371
+ expect(field.normalizeHex("0xDEADBEEF")).toBe("deadbeef")
1372
+ expect(field.normalizeHex("DEADBEEF")).toBe("deadbeef")
1373
+ expect(field.normalizeHex("0x")).toBe("")
1374
+ expect(field.normalizeHex("abc123")).toBe("abc123")
1375
+ }
1376
+ })
1377
+
1378
+ it("validateInput should validate hex strings", () => {
1379
+ const blobType = IDL.Vec(IDL.Nat8)
1380
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1381
+
1382
+ if (field.type === "blob") {
1383
+ // Valid inputs
1384
+ expect(field.validateInput("").valid).toBe(true)
1385
+ expect(field.validateInput("deadbeef").valid).toBe(true)
1386
+ expect(field.validateInput("0x1234").valid).toBe(true)
1387
+
1388
+ // Invalid inputs
1389
+ expect(field.validateInput("xyz").valid).toBe(false)
1390
+ expect(field.validateInput("abc").valid).toBe(false) // odd length
1391
+ }
1392
+ })
1393
+
1394
+ it("validateInput should validate Uint8Array", () => {
1395
+ const blobType = IDL.Vec(IDL.Nat8)
1396
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1397
+
1398
+ if (field.type === "blob") {
1399
+ expect(field.validateInput(new Uint8Array([1, 2, 3])).valid).toBe(true)
1400
+ }
1401
+ })
1402
+ })
1403
+
1404
+ // ════════════════════════════════════════════════════════════════════════
1405
+ // Format Detection
1406
+ // ════════════════════════════════════════════════════════════════════════
1407
+
1408
+ describe("Format Detection", () => {
1409
+ it("should NOT detect eth format for method_name", () => {
1410
+ const field = visitor.visitText(IDL.Text, "method_name")
1411
+ expect(field.format).toBe("plain")
1412
+ })
1413
+
1414
+ it("should detect eth format for my_eth_address", () => {
1415
+ const field = visitor.visitText(IDL.Text, "my_eth_address")
1416
+ expect(field.format).toBe("eth")
1417
+ })
1418
+
1419
+ it("should detect eth format for myEthAddress", () => {
1420
+ const field = visitor.visitText(IDL.Text, "myEthAddress")
1421
+ expect(field.format).toBe("eth")
1422
+ })
1423
+
1424
+ it("should detect eth format for ethereum", () => {
1425
+ const field = visitor.visitText(IDL.Text, "ethereum")
1426
+ expect(field.format).toBe("eth")
1427
+ })
1428
+
1429
+ it("should detect btc format for bitcoin_address", () => {
1430
+ const field = visitor.visitText(IDL.Text, "bitcoin_address")
1431
+ expect(field.format).toBe("btc")
1432
+ })
1433
+
1434
+ it("should NOT detect btc format for debt", () => {
1435
+ const field = visitor.visitText(IDL.Text, "debt")
1436
+ expect(field.format).toBe("plain")
1437
+ })
1438
+ })
1439
+ })