feat(i18n): localize item validation messages
This commit is contained in:
+32
-13
@@ -1,11 +1,13 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
|
|
||||||
|
import { getI18n } from "@/i18n/server"
|
||||||
import {
|
import {
|
||||||
|
buildCreateItemSchema,
|
||||||
|
buildUpdateItemSchema,
|
||||||
type CreateItemFormType,
|
type CreateItemFormType,
|
||||||
createItemSchema,
|
|
||||||
type UpdateItemFormType,
|
type UpdateItemFormType,
|
||||||
updateItemSchema,
|
|
||||||
} from "@/schemas/item.schema"
|
} from "@/schemas/item.schema"
|
||||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||||
import {
|
import {
|
||||||
@@ -14,8 +16,12 @@ import {
|
|||||||
updateItemUseCase,
|
updateItemUseCase,
|
||||||
} from "@/use-cases/item.use-cases"
|
} from "@/use-cases/item.use-cases"
|
||||||
|
|
||||||
|
import { localizeItemFieldErrors } from "./item.messages"
|
||||||
|
|
||||||
export async function createItemAction(formData: CreateItemFormType) {
|
export async function createItemAction(formData: CreateItemFormType) {
|
||||||
const validatedFields = createItemSchema.safeParse(formData)
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items
|
||||||
|
const validatedFields = buildCreateItemSchema(copy.schema).safeParse(formData)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
@@ -32,7 +38,11 @@ export async function createItemAction(formData: CreateItemFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeItemFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.createFailure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/inventory/items")
|
revalidatePath("/inventory/items")
|
||||||
@@ -40,18 +50,20 @@ export async function createItemAction(formData: CreateItemFormType) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Item created successfully!",
|
message: copy.actions.createSuccess,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return {
|
return {
|
||||||
error: "Error creating item",
|
error: copy.actions.createFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateItemAction(formData: UpdateItemFormType) {
|
export async function updateItemAction(formData: UpdateItemFormType) {
|
||||||
const validatedFields = updateItemSchema.safeParse(formData)
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items
|
||||||
|
const validatedFields = buildUpdateItemSchema(copy.schema).safeParse(formData)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
@@ -68,7 +80,11 @@ export async function updateItemAction(formData: UpdateItemFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeItemFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.updateFailure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/inventory/items")
|
revalidatePath("/inventory/items")
|
||||||
@@ -76,17 +92,19 @@ export async function updateItemAction(formData: UpdateItemFormType) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Item updated successfully!",
|
message: copy.actions.updateSuccess,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return {
|
return {
|
||||||
error: "Failed to update item",
|
error: copy.actions.updateFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteItemAction(formData: FormData) {
|
export async function deleteItemAction(formData: FormData) {
|
||||||
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items
|
||||||
const { id } = Object.fromEntries(formData) as { id: string }
|
const { id } = Object.fromEntries(formData) as { id: string }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +113,8 @@ export async function deleteItemAction(formData: FormData) {
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
message: "Failed to delete item",
|
errors: localizeItemFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.deleteFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +122,13 @@ export async function deleteItemAction(formData: FormData) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true as const,
|
success: true as const,
|
||||||
message: "Item deleted successfully!",
|
message: copy.actions.deleteSuccess,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return {
|
return {
|
||||||
success: false as const,
|
success: false as const,
|
||||||
message: "Failed to delete item",
|
message: copy.actions.deleteFailure,
|
||||||
errors: {},
|
errors: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
type ItemActionCopy = Dictionary["inventory"]["items"]["actions"]
|
||||||
|
|
||||||
|
type FieldErrors = Record<string, string[]>
|
||||||
|
|
||||||
|
const itemErrorMessageKeys = {
|
||||||
|
"An item with this name already exists": "duplicateName",
|
||||||
|
"Item not found": "notFound",
|
||||||
|
"Item has assets, you cannot delete it": "hasAssets",
|
||||||
|
"Item has stock, you cannot delete it": "hasStock",
|
||||||
|
"Invalid stock": "invalidStock",
|
||||||
|
"Stock cannot be negative": "negativeStock",
|
||||||
|
} as const satisfies Record<string, keyof ItemActionCopy>
|
||||||
|
|
||||||
|
function isItemErrorMessage(
|
||||||
|
message: string,
|
||||||
|
): message is keyof typeof itemErrorMessageKeys {
|
||||||
|
return message in itemErrorMessageKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function localizeItemMessage(message: string, copy: ItemActionCopy): string {
|
||||||
|
if (!isItemErrorMessage(message)) return message
|
||||||
|
|
||||||
|
return copy[itemErrorMessageKeys[message]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localizeItemFieldErrors(
|
||||||
|
errors: FieldErrors | undefined,
|
||||||
|
copy: ItemActionCopy,
|
||||||
|
): FieldErrors | undefined {
|
||||||
|
if (!errors) return undefined
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(errors).map(([field, messages]) => [
|
||||||
|
field,
|
||||||
|
messages.map((message) => localizeItemMessage(message, copy)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ export default async function AddItem({
|
|||||||
categories={categories}
|
categories={categories}
|
||||||
item={item}
|
item={item}
|
||||||
formCopy={copy.form}
|
formCopy={copy.form}
|
||||||
|
schemaCopy={copy.schema}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Dictionary } from "@/i18n/dictionaries"
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
import type { ItemSchemaCopy } from "@/schemas/item.schema"
|
||||||
|
|
||||||
export type ItemListCopy = Dictionary["inventory"]["items"]["list"]
|
export type ItemListCopy = Dictionary["inventory"]["items"]["list"]
|
||||||
export type ItemDetailCopy = Dictionary["inventory"]["items"]["detail"]
|
export type ItemDetailCopy = Dictionary["inventory"]["items"]["detail"]
|
||||||
export type ItemFormCopy = Dictionary["inventory"]["items"]["form"]
|
export type ItemFormCopy = Dictionary["inventory"]["items"]["form"]
|
||||||
export type ItemDeleteCopy = Dictionary["inventory"]["items"]["delete"]
|
export type ItemDeleteCopy = Dictionary["inventory"]["items"]["delete"]
|
||||||
|
export type { ItemSchemaCopy }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { createItemAction } from "@/actions/item.actions"
|
import { createItemAction } from "@/actions/item.actions"
|
||||||
@@ -10,23 +11,26 @@ import {
|
|||||||
type SubmitButtonCopy,
|
type SubmitButtonCopy,
|
||||||
} from "@/components/forms/submitButton"
|
} from "@/components/forms/submitButton"
|
||||||
import {
|
import {
|
||||||
|
buildCreateItemSchema,
|
||||||
type CreateItemFormType,
|
type CreateItemFormType,
|
||||||
createItemSchema,
|
|
||||||
} from "@/schemas/item.schema"
|
} from "@/schemas/item.schema"
|
||||||
import type { CategorySummary } from "@/types"
|
import type { CategorySummary } from "@/types"
|
||||||
|
|
||||||
import type { ItemFormCopy } from "./item.copy"
|
import type { ItemFormCopy, ItemSchemaCopy } from "./item.copy"
|
||||||
|
|
||||||
export default function NewItemForm({
|
export default function NewItemForm({
|
||||||
categories,
|
categories,
|
||||||
formCopy,
|
formCopy,
|
||||||
|
schemaCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: {
|
}: {
|
||||||
categories: CategorySummary[]
|
categories: CategorySummary[]
|
||||||
formCopy: ItemFormCopy
|
formCopy: ItemFormCopy
|
||||||
|
schemaCopy: ItemSchemaCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const schema = useMemo(() => buildCreateItemSchema(schemaCopy), [schemaCopy])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -34,7 +38,7 @@ export default function NewItemForm({
|
|||||||
setError,
|
setError,
|
||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
formState: { errors, isSubmitting, isSubmitSuccessful },
|
||||||
} = useForm<CreateItemFormType>({
|
} = useForm<CreateItemFormType>({
|
||||||
resolver: zodResolver(createItemSchema),
|
resolver: zodResolver(schema),
|
||||||
shouldFocusError: true,
|
shouldFocusError: true,
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { updateItemAction } from "@/actions/item.actions"
|
import { updateItemAction } from "@/actions/item.actions"
|
||||||
@@ -10,25 +11,28 @@ import {
|
|||||||
type SubmitButtonCopy,
|
type SubmitButtonCopy,
|
||||||
} from "@/components/forms/submitButton"
|
} from "@/components/forms/submitButton"
|
||||||
import {
|
import {
|
||||||
|
buildUpdateItemSchema,
|
||||||
type UpdateItemFormType,
|
type UpdateItemFormType,
|
||||||
updateItemSchema,
|
|
||||||
} from "@/schemas/item.schema"
|
} from "@/schemas/item.schema"
|
||||||
import type { CategorySummary, ItemWithAssetCount } from "@/types"
|
import type { CategorySummary, ItemWithAssetCount } from "@/types"
|
||||||
|
|
||||||
import type { ItemFormCopy } from "./item.copy"
|
import type { ItemFormCopy, ItemSchemaCopy } from "./item.copy"
|
||||||
|
|
||||||
export default function UpdateItemForm({
|
export default function UpdateItemForm({
|
||||||
categories,
|
categories,
|
||||||
item,
|
item,
|
||||||
formCopy,
|
formCopy,
|
||||||
|
schemaCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: {
|
}: {
|
||||||
categories: CategorySummary[]
|
categories: CategorySummary[]
|
||||||
item: ItemWithAssetCount
|
item: ItemWithAssetCount
|
||||||
formCopy: ItemFormCopy
|
formCopy: ItemFormCopy
|
||||||
|
schemaCopy: ItemSchemaCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const schema = useMemo(() => buildUpdateItemSchema(schemaCopy), [schemaCopy])
|
||||||
|
|
||||||
const isDisabled = !!item?._count.assets && item?._count.assets > 0
|
const isDisabled = !!item?._count.assets && item?._count.assets > 0
|
||||||
|
|
||||||
@@ -38,7 +42,7 @@ export default function UpdateItemForm({
|
|||||||
setError,
|
setError,
|
||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
formState: { errors, isSubmitting, isSubmitSuccessful },
|
||||||
} = useForm<UpdateItemFormType>({
|
} = useForm<UpdateItemFormType>({
|
||||||
resolver: zodResolver(updateItemSchema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: item?.id,
|
id: item?.id,
|
||||||
name: item?.name,
|
name: item?.name,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default async function NewItemPage() {
|
|||||||
<NewItemForm
|
<NewItemForm
|
||||||
categories={categories}
|
categories={categories}
|
||||||
formCopy={copy.form}
|
formCopy={copy.form}
|
||||||
|
schemaCopy={copy.schema}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+48
-21
@@ -1,33 +1,60 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const createItemSchema = z.object({
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
name: z.string().min(1, {
|
|
||||||
error: "Name is required",
|
export type ItemSchemaCopy = Dictionary["inventory"]["items"]["schema"]
|
||||||
}),
|
|
||||||
categoryId: z.string().min(1, {
|
const defaultItemSchemaCopy: ItemSchemaCopy = {
|
||||||
error: "Category is required",
|
nameRequired: "Name is required",
|
||||||
}),
|
categoryRequired: "Category is required",
|
||||||
stock: z.coerce.number().int().nonnegative().min(0, {
|
stockRequired: "Stock is required",
|
||||||
error: "Stock is required",
|
itemRequired: "Item is required",
|
||||||
}),
|
}
|
||||||
})
|
|
||||||
|
export function buildCreateItemSchema(copy: ItemSchemaCopy) {
|
||||||
|
return z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
error: copy.nameRequired,
|
||||||
|
}),
|
||||||
|
categoryId: z.string().min(1, {
|
||||||
|
error: copy.categoryRequired,
|
||||||
|
}),
|
||||||
|
stock: z.coerce
|
||||||
|
.number({ error: copy.stockRequired })
|
||||||
|
.int({ error: copy.stockRequired })
|
||||||
|
.nonnegative({ error: copy.stockRequired })
|
||||||
|
.min(0, {
|
||||||
|
error: copy.stockRequired,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createItemSchema = buildCreateItemSchema(defaultItemSchemaCopy)
|
||||||
|
|
||||||
export type CreateItemFormType = z.input<typeof createItemSchema>
|
export type CreateItemFormType = z.input<typeof createItemSchema>
|
||||||
export type CreateItemData = z.output<typeof createItemSchema>
|
export type CreateItemData = z.output<typeof createItemSchema>
|
||||||
|
|
||||||
export const updateItemSchema = createItemSchema.extend({
|
export function buildUpdateItemSchema(copy: ItemSchemaCopy) {
|
||||||
id: z.string().min(1, {
|
return buildCreateItemSchema(copy).extend({
|
||||||
error: "Item is required",
|
id: z.string().min(1, {
|
||||||
}),
|
error: copy.itemRequired,
|
||||||
})
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateItemSchema = buildUpdateItemSchema(defaultItemSchemaCopy)
|
||||||
|
|
||||||
export type UpdateItemFormType = z.input<typeof updateItemSchema>
|
export type UpdateItemFormType = z.input<typeof updateItemSchema>
|
||||||
export type UpdateItemData = z.output<typeof updateItemSchema>
|
export type UpdateItemData = z.output<typeof updateItemSchema>
|
||||||
|
|
||||||
export const getItemByIdSchema = z.object({
|
export function buildGetItemByIdSchema(copy: ItemSchemaCopy) {
|
||||||
id: z.string().min(1, {
|
return z.object({
|
||||||
error: "Item is required",
|
id: z.string().min(1, {
|
||||||
}),
|
error: copy.itemRequired,
|
||||||
})
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getItemByIdSchema = buildGetItemByIdSchema(defaultItemSchemaCopy)
|
||||||
|
|
||||||
export type GetItemByIdFormType = z.infer<typeof getItemByIdSchema>
|
export type GetItemByIdFormType = z.infer<typeof getItemByIdSchema>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { localizeItemFieldErrors } from "@/actions/item.messages"
|
||||||
|
|
||||||
|
const actionCopy = {
|
||||||
|
createSuccess: "Artículo creado correctamente",
|
||||||
|
createFailure: "Error al crear el artículo",
|
||||||
|
updateSuccess: "Artículo actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el artículo",
|
||||||
|
deleteSuccess: "Artículo eliminado correctamente",
|
||||||
|
deleteFailure: "Error al eliminar el artículo",
|
||||||
|
duplicateName: "El artículo ya existe",
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
hasAssets: "No se puede eliminar un artículo con activos",
|
||||||
|
hasStock: "No se puede eliminar un artículo con stock",
|
||||||
|
invalidStock: "Stock inválido",
|
||||||
|
negativeStock: "El stock no puede ser negativo",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("item action message localization", () => {
|
||||||
|
it("localizes known item field errors", () => {
|
||||||
|
expect(
|
||||||
|
localizeItemFieldErrors(
|
||||||
|
{
|
||||||
|
name: ["An item with this name already exists"],
|
||||||
|
id: [
|
||||||
|
"Item not found",
|
||||||
|
"Item has assets, you cannot delete it",
|
||||||
|
"Item has stock, you cannot delete it",
|
||||||
|
],
|
||||||
|
stock: ["Stock cannot be negative", "Invalid stock"],
|
||||||
|
},
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
name: [actionCopy.duplicateName],
|
||||||
|
id: [actionCopy.notFound, actionCopy.hasAssets, actionCopy.hasStock],
|
||||||
|
stock: [actionCopy.negativeStock, actionCopy.invalidStock],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps unknown messages unchanged", () => {
|
||||||
|
expect(
|
||||||
|
localizeItemFieldErrors({ name: ["Unexpected item issue"] }, actionCopy),
|
||||||
|
).toEqual({ name: ["Unexpected item issue"] })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildCreateItemSchema,
|
||||||
|
buildGetItemByIdSchema,
|
||||||
|
buildUpdateItemSchema,
|
||||||
|
} from "@/schemas/item.schema"
|
||||||
|
|
||||||
|
const schemaCopy = {
|
||||||
|
nameRequired: "El nombre es obligatorio",
|
||||||
|
categoryRequired: "La categoría es obligatoria",
|
||||||
|
stockRequired: "El stock es obligatorio",
|
||||||
|
itemRequired: "El artículo es obligatorio",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("item schema localization", () => {
|
||||||
|
it("uses localized create validation messages", () => {
|
||||||
|
const result = buildCreateItemSchema(schemaCopy).safeParse({
|
||||||
|
name: "",
|
||||||
|
categoryId: "",
|
||||||
|
stock: -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten().fieldErrors
|
||||||
|
|
||||||
|
expect(errors.name).toContain(schemaCopy.nameRequired)
|
||||||
|
expect(errors.categoryId).toContain(schemaCopy.categoryRequired)
|
||||||
|
expect(errors.stock).toContain(schemaCopy.stockRequired)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses localized update identifier validation messages", () => {
|
||||||
|
const result = buildUpdateItemSchema(schemaCopy).safeParse({
|
||||||
|
id: "",
|
||||||
|
name: "Laptop",
|
||||||
|
categoryId: "category-1",
|
||||||
|
stock: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error.flatten().fieldErrors.id).toContain(
|
||||||
|
schemaCopy.itemRequired,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses localized get-by-id validation messages", () => {
|
||||||
|
const result = buildGetItemByIdSchema(schemaCopy).safeParse({ id: "" })
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error.flatten().fieldErrors.id).toContain(
|
||||||
|
schemaCopy.itemRequired,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves stock coercion and integer validation semantics", () => {
|
||||||
|
const validResult = buildCreateItemSchema(schemaCopy).safeParse({
|
||||||
|
name: "Laptop",
|
||||||
|
categoryId: "category-1",
|
||||||
|
stock: "2",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(validResult.success).toBe(true)
|
||||||
|
if (validResult.success) {
|
||||||
|
expect(validResult.data.stock).toBe(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidResult = buildCreateItemSchema(schemaCopy).safeParse({
|
||||||
|
name: "Laptop",
|
||||||
|
categoryId: "category-1",
|
||||||
|
stock: 1.5,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(invalidResult.success).toBe(false)
|
||||||
|
if (!invalidResult.success) {
|
||||||
|
expect(invalidResult.error.flatten().fieldErrors.stock).toContain(
|
||||||
|
schemaCopy.stockRequired,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user