@zeno-lib/forms 0.9.1 → 0.10.1

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 (84) hide show
  1. package/dist/create-zeno-form.d.mts +1 -2
  2. package/dist/create-zeno-form.mjs +1 -2
  3. package/dist/form-element.d.mts +0 -2
  4. package/dist/form-element.mjs +1 -2
  5. package/dist/lib/action-result.d.mts +0 -2
  6. package/dist/lib/action-result.mjs +1 -2
  7. package/dist/lib/apply-validation-error.d.mts +0 -2
  8. package/dist/lib/apply-validation-error.mjs +1 -2
  9. package/dist/lib/aria.d.mts +1 -3
  10. package/dist/lib/aria.mjs +1 -2
  11. package/dist/lib/contexts.d.mts +0 -2
  12. package/dist/lib/contexts.mjs +1 -2
  13. package/dist/lib/date-value.d.mts +0 -2
  14. package/dist/lib/date-value.mjs +2 -3
  15. package/dist/lib/default-props.d.mts +1 -3
  16. package/dist/lib/default-props.mjs +1 -2
  17. package/dist/lib/empty-value.d.mts +9 -3
  18. package/dist/lib/empty-value.mjs +15 -5
  19. package/dist/lib/field-hints.d.mts +16 -0
  20. package/dist/lib/field-hints.mjs +23 -0
  21. package/dist/lib/focus-first-invalid.d.mts +0 -2
  22. package/dist/lib/focus-first-invalid.mjs +1 -2
  23. package/dist/lib/form-disabled.d.mts +0 -2
  24. package/dist/lib/form-disabled.mjs +1 -2
  25. package/dist/lib/formatted-number.d.mts +0 -2
  26. package/dist/lib/formatted-number.mjs +10 -11
  27. package/dist/lib/item-options.d.mts +0 -2
  28. package/dist/lib/item-options.mjs +1 -2
  29. package/dist/lib/schema-defaults.d.mts +6 -3
  30. package/dist/lib/schema-defaults.mjs +37 -5
  31. package/dist/lib/schema-required.d.mts +0 -2
  32. package/dist/lib/schema-required.mjs +3 -4
  33. package/dist/lib/submit-action.d.mts +0 -2
  34. package/dist/lib/submit-action.mjs +1 -2
  35. package/dist/lib/submit-error.d.mts +0 -2
  36. package/dist/lib/submit-error.mjs +1 -2
  37. package/dist/lib/use-field-control.d.mts +0 -2
  38. package/dist/lib/use-field-control.mjs +1 -2
  39. package/dist/lib/use-form-dialog.d.mts +0 -2
  40. package/dist/lib/use-form-dialog.mjs +2 -3
  41. package/dist/lib/use-formatted-number.d.mts +0 -2
  42. package/dist/lib/use-formatted-number.mjs +1 -2
  43. package/dist/lib/use-is-invalid.d.mts +0 -2
  44. package/dist/lib/use-is-invalid.mjs +1 -2
  45. package/dist/lib/use-rebased-default-values.d.mts +0 -2
  46. package/dist/lib/use-rebased-default-values.mjs +1 -2
  47. package/dist/lib/use-unsaved-changes-warning.d.mts +0 -2
  48. package/dist/lib/use-unsaved-changes-warning.mjs +1 -2
  49. package/dist/lib/validation-error.d.mts +0 -2
  50. package/dist/lib/validation-error.mjs +1 -2
  51. package/dist/lib/validation-logic.d.mts +0 -2
  52. package/dist/lib/validation-logic.mjs +1 -2
  53. package/dist/lib/validation-modes.d.mts +0 -2
  54. package/dist/lib/validation-modes.mjs +1 -2
  55. package/package.json +8 -8
  56. package/src/composition.test.tsx +3 -3
  57. package/src/control-field.test.tsx +1 -1
  58. package/src/date-picker-field.test.tsx +3 -3
  59. package/src/field-hints.test.tsx +118 -0
  60. package/src/fields/input-field.tsx +6 -4
  61. package/src/fields/percentage-field.tsx +16 -8
  62. package/src/fields/textarea-field.tsx +6 -4
  63. package/src/fields/year-field.tsx +8 -3
  64. package/src/form-dialog.test.tsx +3 -3
  65. package/src/form-error.test.tsx +1 -1
  66. package/src/formatted-number-fields.test.tsx +3 -3
  67. package/src/lib/date-value.ts +2 -1
  68. package/src/lib/empty-value.ts +26 -3
  69. package/src/lib/field-hints.ts +64 -0
  70. package/src/lib/formatted-number.ts +9 -9
  71. package/src/lib/schema-defaults.test.ts +6 -3
  72. package/src/lib/schema-defaults.ts +60 -3
  73. package/src/lib/schema-required.ts +2 -2
  74. package/src/lib/use-form-dialog.ts +1 -1
  75. package/src/multi-value-fields.test.tsx +2 -2
  76. package/src/nullable-text.test.tsx +80 -0
  77. package/src/submit-action.test.tsx +4 -4
  78. package/src/use-app-fields.test.tsx +10 -10
  79. package/src/use-form.cascade.test.tsx +2 -2
  80. package/src/use-form.reset-defaults.test.tsx +2 -2
  81. package/src/use-form.test.tsx +3 -3
  82. package/src/use-form.unsaved-warning.test.tsx +1 -1
  83. package/src/use-form.validation-modes.test.tsx +2 -2
  84. package/src/with-form.test.tsx +1 -1
@@ -0,0 +1,64 @@
1
+ import type { AnyFieldApi } from "@tanstack/react-form"
2
+ import { toPathKeys } from "./empty-value"
3
+ import { findZodSchemaAt } from "./schema-defaults"
4
+ import { getFormSchema } from "./validation-modes"
5
+
6
+ /**
7
+ * What a field reads from the column its schema comes from, when the column's
8
+ * hint names it. The `percentage()` and `year()` columns of
9
+ * `@zeno-lib/db/schema` give one, so `PercentageField` and `YearField` show
10
+ * and clamp what the column accepts with no props: the scale from the hint,
11
+ * the bounds from the schema's own `.min()` and `.max()`, a caller's
12
+ * narrowing included. A prop the field is given wins.
13
+ */
14
+ type FieldHints = {
15
+ readonly max?: number | undefined
16
+ readonly min?: number | undefined
17
+ readonly scale?: "fraction" | "percent" | undefined
18
+ }
19
+
20
+ // The registry `@zeno-lib/schema` puts each column's hint in, under a
21
+ // `globalThis` symbol both packages name, so this one reads it without
22
+ // importing that one. Absent until `@zeno-lib/schema` loads, and then no
23
+ // field has a hint.
24
+ const FIELD_HINTS_KEY = Symbol.for("@zeno-lib/field-hints")
25
+
26
+ type HintRegistry = { readonly get: (schema: object) => unknown }
27
+
28
+ type Bounded = { readonly maxValue?: unknown; readonly minValue?: unknown }
29
+
30
+ const readHint = (node: object) => {
31
+ const registry = (globalThis as { [FIELD_HINTS_KEY]?: HintRegistry })[
32
+ FIELD_HINTS_KEY
33
+ ]
34
+ const hint = registry?.get(node) as { field?: unknown } | undefined
35
+ return typeof hint?.field === "string" ? hint : undefined
36
+ }
37
+
38
+ // A schema with no `.min()` or `.max()` reports `null` or an infinity.
39
+ const toBound = (value: unknown) =>
40
+ typeof value === "number" && Number.isFinite(value) ? value : undefined
41
+
42
+ function getFieldHints(field: AnyFieldApi, kind: string): FieldHints {
43
+ const schema = getFormSchema(field.form)
44
+ const found = schema
45
+ ? findZodSchemaAt(
46
+ schema as Parameters<typeof findZodSchemaAt>[0],
47
+ toPathKeys(field.name),
48
+ readHint
49
+ )
50
+ : undefined
51
+ if (found?.value.field !== kind) {
52
+ return {}
53
+ }
54
+ const { scale } = found.value as { scale?: unknown }
55
+ const { maxValue, minValue } = found.node as Bounded
56
+ return {
57
+ max: toBound(maxValue),
58
+ min: toBound(minValue),
59
+ scale: scale === "fraction" || scale === "percent" ? scale : undefined,
60
+ }
61
+ }
62
+
63
+ export type { FieldHints }
64
+ export { getFieldHints }
@@ -43,15 +43,15 @@ const separatorCache = new Map<string, NumberSeparators>()
43
43
 
44
44
  // Minus signs a user may type or paste: ASCII hyphen-minus, U+2212 (what Intl
45
45
  // emits for some locales), and the en dash.
46
- const MINUS_SIGNS = /[-−–]/
47
- const LEADING_MINUS = /^[^\d]*[-−–]/
48
- const NON_DIGIT = /\D/g
49
- const LEADING_ZEROS = /^0+(?=\d)/
50
- const DIGIT = /\d/
51
- const DIGITS = /\d/g
52
- const PARENTHESIZED = /^\s*\(.*\)\s*$/
53
- const NOT_NUMERIC = /[^\d.,]/g
54
- const UNICODE_MINUS = /\u2212/g
46
+ const MINUS_SIGNS = /[-−–]/u
47
+ const LEADING_MINUS = /^[^\d]*[-−–]/u
48
+ const NON_DIGIT = /\D/gu
49
+ const LEADING_ZEROS = /^0+(?=\d)/u
50
+ const DIGIT = /\d/u
51
+ const DIGITS = /\d/gu
52
+ const PARENTHESIZED = /^\s*\(.*\)\s*$/u
53
+ const NOT_NUMERIC = /[^\d.,]/gu
54
+ const UNICODE_MINUS = /\u2212/gu
55
55
 
56
56
  function getNumberSeparators(locale?: string): NumberSeparators {
57
57
  const key = locale ?? ""
@@ -65,9 +65,12 @@ describe("extractZodDefaults — wrappers", () => {
65
65
  expect(extractZodDefaults(schema)).toEqual({ x: "" })
66
66
  })
67
67
 
68
- test(".nullable() unwraps to inner string → ''", () => {
69
- const schema = z.object({ x: z.string().nullable() })
70
- expect(extractZodDefaults(schema)).toEqual({ x: "" })
68
+ test(".nullable() starts a string at null, which a nullable column stores", () => {
69
+ const schema = z.object({
70
+ x: z.string().nullable(),
71
+ y: z.string().nullable().optional(),
72
+ })
73
+ expect(extractZodDefaults(schema)).toEqual({ x: null, y: null })
71
74
  })
72
75
 
73
76
  test(".readonly() unwraps to inner string → ''", () => {
@@ -4,7 +4,8 @@
4
4
  // Tier A+ semantics:
5
5
  // - `.default(...)` / `.prefault(...)` values flow through.
6
6
  // - Strings (and string-format subtypes — email, url, uuid, regex, …) without
7
- // a default fall back to `""` so React inputs render controlled.
7
+ // a default fall back to `""` so React inputs render controlled, or to
8
+ // `null` under `.nullable()`, which the fields render as no text.
8
9
  // - Arrays without a default fall back to `[]`.
9
10
  // - Nested `z.object(...)` recurses; emits a sub-record only if at least one
10
11
  // leaf produced a value.
@@ -26,6 +27,7 @@ type SchemaDef = {
26
27
  readonly shape?: Record<string, SchemaNode>
27
28
  readonly catchall?: SchemaNode
28
29
  readonly in?: SchemaNode
30
+ readonly out?: SchemaNode
29
31
  readonly element?: SchemaNode
30
32
  readonly defaultValue?: unknown
31
33
  }
@@ -40,6 +42,7 @@ function getDef(node: SchemaNode | undefined): SchemaDef | undefined {
40
42
 
41
43
  function extractDefaultForField(node: SchemaNode): ExtractResult {
42
44
  let current: SchemaNode | undefined = node
45
+ let isNullable = false
43
46
  for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth += 1) {
44
47
  const def: SchemaDef | undefined = getDef(current)
45
48
  if (!def?.type) {
@@ -49,8 +52,11 @@ function extractDefaultForField(node: SchemaNode): ExtractResult {
49
52
  case "default":
50
53
  case "prefault":
51
54
  return { ok: true, value: def.defaultValue }
52
- case "optional":
53
55
  case "nullable":
56
+ isNullable = true
57
+ current = def.innerType
58
+ continue
59
+ case "optional":
54
60
  case "nonoptional":
55
61
  case "readonly":
56
62
  current = def.innerType
@@ -58,8 +64,10 @@ function extractDefaultForField(node: SchemaNode): ExtractResult {
58
64
  case "pipe":
59
65
  current = def.in
60
66
  continue
67
+ // No text is `null` where the schema takes it, as a nullable column
68
+ // stores it, so a field left blank saves as none rather than as `""`.
61
69
  case "string":
62
- return { ok: true, value: "" }
70
+ return { ok: true, value: isNullable ? null : "" }
63
71
  case "array":
64
72
  return { ok: true, value: [] }
65
73
  case "object": {
@@ -142,6 +150,54 @@ function extractZodDefaultAt(
142
150
  }
143
151
  }
144
152
 
153
+ // The schema inside a wrapper, or `undefined` past the last one.
154
+ function unwrapped(node: SchemaNode): SchemaNode | undefined {
155
+ const def: SchemaDef | undefined = getDef(node)
156
+ switch (def?.type) {
157
+ case "catch":
158
+ case "default":
159
+ case "nonoptional":
160
+ case "nullable":
161
+ case "optional":
162
+ case "prefault":
163
+ case "readonly":
164
+ return def.innerType
165
+ // `z.preprocess` pipes a transform into the schema it feeds.
166
+ case "pipe":
167
+ return getDef(def.in)?.type === "transform" ? def.out : def.in
168
+ default:
169
+ return undefined
170
+ }
171
+ }
172
+
173
+ // The schema at `keys`, or inside one of its wrappers, that `read` finds a
174
+ // value on, with that value. A Zod registry keeps an entry on the schema it
175
+ // was set on, not on the `.optional()` or `.nullable()` around it.
176
+ function findZodSchemaAt<T>(
177
+ schema: SchemaNode,
178
+ keys: readonly PropertyKey[],
179
+ read: (node: SchemaNode) => T | undefined
180
+ ): { node: SchemaNode; value: T } | undefined {
181
+ try {
182
+ let node: SchemaNode | null = schema
183
+ for (const key of keys) {
184
+ node = node && childAt(node, key)
185
+ }
186
+ let current: SchemaNode | undefined =
187
+ node && keys.length > 0 ? node : undefined
188
+ for (let depth = 0; current && depth < MAX_WRAPPER_DEPTH; depth += 1) {
189
+ const value = read(current)
190
+ if (value !== undefined) {
191
+ return { node: current, value }
192
+ }
193
+ current = unwrapped(current)
194
+ }
195
+ } catch {
196
+ // An unrecognised schema has nothing to offer.
197
+ }
198
+ return undefined
199
+ }
200
+
145
201
  function isPlainObject(value: unknown): value is Record<string, unknown> {
146
202
  return (
147
203
  typeof value === "object" &&
@@ -244,5 +300,6 @@ export {
244
300
  deepMergeDefaults,
245
301
  extractZodDefaultAt,
246
302
  extractZodDefaults,
303
+ findZodSchemaAt,
247
304
  pickSchemaShape,
248
305
  }
@@ -53,10 +53,10 @@ type StandardSchemaLike = {
53
53
  // Each level can take two passes: the empty value, then the descent.
54
54
  const MAX_PROBE_DEPTH = 16
55
55
 
56
- const INDEX_SEGMENT = /^\d+$/
56
+ const INDEX_SEGMENT = /^\d+$/u
57
57
 
58
58
  // Matches `[3]` and `.3` index segments in a field name.
59
- const INDEX_IN_NAME = /\[\d+\]|\.(\d+)(?=\.|\[|$)/g
59
+ const INDEX_IN_NAME = /\[\d+\]|\.\d+(?=\.|\[|$)/gu
60
60
 
61
61
  function pathKey(part: PathPart): PropertyKey {
62
62
  // biome-ignore lint/suspicious/noUnnecessaryConditions: issue paths come from third-party validators at runtime, and a non-conforming `null` segment would make `in` throw.
@@ -199,7 +199,7 @@ const FOCUSABLE =
199
199
  'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
200
200
 
201
201
  function attributeSelector(attribute: string, value: string): string {
202
- return `[${attribute}="${value.replace(/["\\]/g, "\\$&")}"]`
202
+ return `[${attribute}="${value.replace(/["\\]/gu, "\\$&")}"]`
203
203
  }
204
204
 
205
205
  /**
@@ -3,8 +3,8 @@ import userEvent from "@zeno-lib/test/user-event"
3
3
  import { afterEach, describe, expect, test, vi } from "vitest"
4
4
  import { Form, FormProvider, useForm } from "./create-form"
5
5
 
6
- const TAGS = /Tags/
7
- const CLEAR = /Clear/
6
+ const TAGS = /Tags/u
7
+ const CLEAR = /Clear/u
8
8
 
9
9
  afterEach(() => {
10
10
  cleanup()
@@ -0,0 +1,80 @@
1
+ import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
+ import userEvent from "@zeno-lib/test/user-event"
3
+ import { afterEach, expect, test, vi } from "vitest"
4
+ import { z } from "zod"
5
+ import { Form, FormProvider, useForm } from "./create-form"
6
+
7
+ afterEach(cleanup)
8
+
9
+ test("a nullable text field holds null for no text, from the start and once emptied", async () => {
10
+ const user = userEvent.setup()
11
+ const schema = z.object({
12
+ note: z.string().nullable(),
13
+ optional: z.string().optional(),
14
+ title: z.string(),
15
+ })
16
+ const captured: { values?: unknown } = {}
17
+ function H() {
18
+ const form = useForm({ onSubmit: vi.fn(), schema })
19
+ const { InputField, Subscribe, TextAreaField } = form
20
+ return (
21
+ <FormProvider form={form}>
22
+ <Form>
23
+ <TextAreaField label="Note" name="note" />
24
+ <InputField label="Optional" name="optional" />
25
+ <InputField label="Title" name="title" />
26
+ <Subscribe selector={(state) => state.values}>
27
+ {(values) => {
28
+ captured.values = values
29
+ return null
30
+ }}
31
+ </Subscribe>
32
+ </Form>
33
+ </FormProvider>
34
+ )
35
+ }
36
+ render(<H />)
37
+ // An optional field keeps `""`: an update skips an `undefined` column, so
38
+ // emptying it would keep the stored text.
39
+ const empty = { note: null, optional: "", title: "" }
40
+ expect(captured.values).toStrictEqual(empty)
41
+
42
+ for (const label of ["Note", "Optional", "Title"]) {
43
+ // biome-ignore lint/performance/noAwaitInLoops: one field after another, as a user types
44
+ await user.type(screen.getByLabelText(label), "x")
45
+ await user.clear(screen.getByLabelText(label))
46
+ }
47
+ expect(captured.values).toStrictEqual(empty)
48
+ })
49
+
50
+ test("an emptied nullable text field holds null whatever the other fields hold", async () => {
51
+ const user = userEvent.setup()
52
+ // Once every field passes, the async refinement keeps the form from being
53
+ // checked synchronously.
54
+ const schema = z
55
+ .object({ note: z.string().nullable() })
56
+ .refine(async () => await Promise.resolve(true))
57
+ const captured: { values?: unknown } = {}
58
+ function H() {
59
+ const form = useForm({ onSubmit: vi.fn(), schema })
60
+ const { InputField, Subscribe } = form
61
+ return (
62
+ <FormProvider form={form}>
63
+ <Form>
64
+ <InputField label="Note" name="note" />
65
+ <Subscribe selector={(state) => state.values}>
66
+ {(values) => {
67
+ captured.values = values
68
+ return null
69
+ }}
70
+ </Subscribe>
71
+ </Form>
72
+ </FormProvider>
73
+ )
74
+ }
75
+ render(<H />)
76
+
77
+ await user.type(screen.getByRole("textbox"), "x")
78
+ await user.clear(screen.getByRole("textbox"))
79
+ expect(captured.values).toStrictEqual({ note: null })
80
+ })
@@ -14,10 +14,10 @@ afterEach(() => {
14
14
  })
15
15
 
16
16
  const SUBMIT = { name: "Submit" }
17
- const EMAIL_LABEL = /Email/
18
- const NAME_LABEL = /Name/
19
- const OWNER_1_SHARE_LABEL = /^Owner 1 share/
20
- const OWNER_2_SHARE_LABEL = /^Owner 2 share/
17
+ const EMAIL_LABEL = /Email/u
18
+ const NAME_LABEL = /Name/u
19
+ const OWNER_1_SHARE_LABEL = /^Owner 1 share/u
20
+ const OWNER_2_SHARE_LABEL = /^Owner 2 share/u
21
21
 
22
22
  function failure(
23
23
  fieldErrors: Record<string, string[]>,
@@ -12,16 +12,16 @@ import { z } from "zod"
12
12
  import { SelectItem } from "@/components/ui/select"
13
13
  import { Form, FormProvider, RadioGroupFieldItem, useForm } from "./create-form"
14
14
 
15
- const CONTACT_EMAIL_LABEL = /Contact email/
16
- const PASSWORD_LABEL = /Password/
17
- const NAME_LABEL = /Name/
18
- const ACCEPT_NAME = /Accept/
19
- const TITLE_LABEL = /Title/
20
- const NOTE_LABEL = /Note/
21
- const DAY_LABEL = /Day/
22
- const PICKED_DAY = /October 9/
23
- const ROLE_LABEL = /Role/
24
- const OWNER_LABEL = /Owner/
15
+ const CONTACT_EMAIL_LABEL = /Contact email/u
16
+ const PASSWORD_LABEL = /Password/u
17
+ const NAME_LABEL = /Name/u
18
+ const ACCEPT_NAME = /Accept/u
19
+ const TITLE_LABEL = /Title/u
20
+ const NOTE_LABEL = /Note/u
21
+ const DAY_LABEL = /Day/u
22
+ const PICKED_DAY = /October 9/u
23
+ const ROLE_LABEL = /Role/u
24
+ const OWNER_LABEL = /Owner/u
25
25
  const PICK_A_ROLE = "Pick a role"
26
26
  const PICK_A_DAY = "Pick a day"
27
27
 
@@ -4,8 +4,8 @@ import { afterEach, describe, expect, test, vi } from "vitest"
4
4
  import { z } from "zod"
5
5
  import { Form, FormProvider, useForm } from "./create-form"
6
6
 
7
- const DIET_LABEL = /Diet/
8
- const SAUSAGE_LABEL = /Sausage/
7
+ const DIET_LABEL = /Diet/u
8
+ const SAUSAGE_LABEL = /Sausage/u
9
9
 
10
10
  afterEach(() => {
11
11
  cleanup()
@@ -14,8 +14,8 @@ afterEach(() => {
14
14
  cleanup()
15
15
  })
16
16
 
17
- const TITLE = /Title/
18
- const SAVE = /Save/
17
+ const TITLE = /Title/u
18
+ const SAVE = /Save/u
19
19
 
20
20
  const schema = z.object({ title: z.string().min(1) })
21
21
 
@@ -6,9 +6,9 @@ import { z } from "zod"
6
6
  import { Form, FormProvider, useForm } from "./create-form"
7
7
  import { ValidationError } from "./lib/validation-error"
8
8
 
9
- const EMAIL_LABEL = /Email/
10
- const VALID_EMAIL_MSG = /Enter a valid email/i
11
- const SUBMIT_NAME = /Submit/
9
+ const EMAIL_LABEL = /Email/u
10
+ const VALID_EMAIL_MSG = /Enter a valid email/iu
11
+ const SUBMIT_NAME = /Submit/u
12
12
 
13
13
  // @testing-library auto-cleanup only runs with `globals: true`. Our vitest
14
14
  // config keeps globals off (we import test helpers explicitly), so add the
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
4
4
  import { z } from "zod"
5
5
  import { Form, FormProvider, useForm } from "./create-form"
6
6
 
7
- const NAME_LABEL = /Name/
7
+ const NAME_LABEL = /Name/u
8
8
 
9
9
  let addSpy: ReturnType<typeof vi.spyOn>
10
10
  let removeSpy: ReturnType<typeof vi.spyOn>
@@ -30,8 +30,8 @@ function Harness({ mode }: { mode?: ValidationMode }) {
30
30
  )
31
31
  }
32
32
 
33
- const ERROR = /Enter a valid email/i
34
- const EMAIL_LABEL = /Email/
33
+ const ERROR = /Enter a valid email/iu
34
+ const EMAIL_LABEL = /Email/u
35
35
 
36
36
  function getInput() {
37
37
  return screen.getByLabelText(EMAIL_LABEL) as HTMLInputElement
@@ -4,7 +4,7 @@ import { afterEach, expect, test, vi } from "vitest"
4
4
  import { z } from "zod"
5
5
  import { Form, FormProvider, useForm, withForm } from "./create-form"
6
6
 
7
- const CITY = /City/
7
+ const CITY = /City/u
8
8
 
9
9
  afterEach(() => {
10
10
  cleanup()