form.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { expect, test } from "bun:test"
  2. import type { FormField, FormValue } from "@opencode-ai/client"
  3. import {
  4. formCustom,
  5. formDisplayValue,
  6. formInitialValues,
  7. formLabel,
  8. formRows,
  9. formSelected,
  10. formSetMultiselectCustom,
  11. formTextual,
  12. formToggleMultiselect,
  13. formValidateValue,
  14. isFormAnswerField,
  15. } from "../../src/util/form"
  16. import type { FormAnswerField } from "../../src/util/form"
  17. const option = {
  18. key: "choice",
  19. type: "string",
  20. options: [{ value: "one", label: "One" }],
  21. custom: true,
  22. } satisfies FormField
  23. const selection = {
  24. key: "tags",
  25. type: "multiselect",
  26. options: [
  27. { value: "one", label: "One" },
  28. { value: "two", label: "Two" },
  29. ],
  30. custom: true,
  31. } satisfies FormAnswerField
  32. test("initializes configured and custom defaults", () => {
  33. expect(
  34. formInitialValues([
  35. { key: "mode", type: "string", options: [{ value: "fast", label: "Fast" }], default: "fast" },
  36. { ...option, key: "note", default: "detailed" },
  37. { ...option, key: "configured", default: "one" },
  38. { key: "count", type: "number", default: 0 },
  39. { key: "authorize", type: "external", url: "https://example.com" },
  40. ]),
  41. ).toEqual({
  42. answers: { mode: "fast", note: "detailed", configured: "one", count: 0 },
  43. custom: { note: "detailed" },
  44. })
  45. })
  46. test("validates every supported field constraint", () => {
  47. const validate = (field: FormAnswerField, value: FormValue | undefined, error: string | undefined) =>
  48. expect(formValidateValue(field, value)).toBe(error)
  49. const string = (extra: Partial<Extract<FormAnswerField, { type: "string" }>> = {}) =>
  50. ({ key: "value", type: "string", ...extra }) satisfies FormAnswerField
  51. const multi = (extra: Partial<Extract<FormAnswerField, { type: "multiselect" }>> = {}) =>
  52. ({ key: "value", type: "multiselect", options: [], ...extra }) satisfies FormAnswerField
  53. validate(string({ required: true }), undefined, "Answer required")
  54. validate(multi({ required: true }), [], "Select at least one option")
  55. validate(string(), true, "Expected text")
  56. validate(string({ minLength: 3 }), "ab", "Must be at least 3 characters")
  57. validate(string({ maxLength: 2 }), "abc", "Must be at most 2 characters")
  58. validate(string({ pattern: "^a+$" }), "bbb", "Must match pattern: ^a+$")
  59. validate(string({ pattern: "[" }), "value", "Invalid pattern: [")
  60. validate(string({ format: "email" }), "invalid", "Expected an email address")
  61. validate(string({ format: "uri" }), "not a URL", "Expected a URL")
  62. validate(string({ format: "date" }), "2025-02-29", "Expected a date (YYYY-MM-DD)")
  63. validate(string({ format: "date-time" }), "not a date", "Expected a date and time")
  64. validate(string({ options: [{ value: "yes", label: "Yes" }] }), "no", "Select an available option")
  65. validate({ key: "value", type: "number" }, Number.NaN, "Expected a number")
  66. validate({ key: "value", type: "integer" }, 1.5, "Expected an integer")
  67. validate({ key: "value", type: "number", minimum: 2 }, 1, "Must be at least 2")
  68. validate({ key: "value", type: "number", maximum: 2 }, 3, "Must be at most 2")
  69. validate({ key: "value", type: "boolean" }, "yes", "Expected yes or no")
  70. validate(multi(), "yes", "Expected selections")
  71. validate(multi({ minItems: 2 }), ["one"], "Select at least 2")
  72. validate(multi({ maxItems: 1 }), ["one", "two"], "Select at most 1")
  73. validate(multi({ options: [{ value: "one", label: "One" }] }), ["two"], "Select only available options")
  74. validate(multi({ custom: true }), ["custom"], undefined)
  75. })
  76. test("shares field classification, rows, selection, and display", () => {
  77. const text = { key: "name", type: "string", title: "Name" } satisfies FormField
  78. const external = { key: "authorize", type: "external", url: "https://example.com" } satisfies FormField
  79. expect([isFormAnswerField(text), isFormAnswerField(external)]).toEqual([true, false])
  80. expect([formLabel(text), formLabel(external)]).toEqual(["Name", "https://example.com"])
  81. expect([formTextual(text), formTextual(option), formCustom(option)]).toEqual([true, false, true])
  82. expect(formRows({ key: "value", type: "boolean" })).toEqual([
  83. { value: true, label: "Yes" },
  84. { value: false, label: "No" },
  85. ])
  86. expect(formRows({ ...option, options: [{ value: "one", label: "One", description: "First" }] })).toEqual([
  87. { value: "one", label: "One", description: "First" },
  88. ])
  89. expect(formRows({ key: "value", type: "number" })).toEqual([])
  90. expect([
  91. formSelected(selection, "two"),
  92. formSelected(selection, "custom"),
  93. formSelected(selection, undefined),
  94. ]).toEqual([1, 2, 0])
  95. expect(formDisplayValue(selection, ["one", "custom"], "(none)")).toBe("One, custom")
  96. expect([formDisplayValue(selection, [], ""), formDisplayValue(selection, [], "(none)")]).toEqual(["", "(none)"])
  97. })
  98. test("updates multiselects without mutating their source", () => {
  99. const source = ["one", "custom"]
  100. expect(formToggleMultiselect(source, "one")).toEqual(["custom"])
  101. expect(formToggleMultiselect(source, "two")).toEqual(["one", "custom", "two"])
  102. expect(formSetMultiselectCustom(source, "custom", "replacement")).toEqual(["one", "replacement"])
  103. expect(source).toEqual(["one", "custom"])
  104. })