feat(assignments): partial return action and ReturnButton modal
This commit is contained in:
@@ -6,6 +6,7 @@ import { localizeAssignmentFieldErrors } from "@/actions/assignment.messages"
|
|||||||
import { getI18n } from "@/i18n/server"
|
import { getI18n } from "@/i18n/server"
|
||||||
import {
|
import {
|
||||||
buildCreateAssignmentSchema,
|
buildCreateAssignmentSchema,
|
||||||
|
buildReturnAssignmentSchema,
|
||||||
buildUpdateAssignmentSchema,
|
buildUpdateAssignmentSchema,
|
||||||
type CreateAssignmentFormType,
|
type CreateAssignmentFormType,
|
||||||
type ReturnAssignmentFormType,
|
type ReturnAssignmentFormType,
|
||||||
@@ -120,14 +121,32 @@ export async function updateAssignment(formData: UpdateAssignmentFormType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function returnAssignment(formData: ReturnAssignmentFormType) {
|
type ReturnAssignmentActionResult =
|
||||||
|
| { success: true; message: string }
|
||||||
|
| { success: false; errors?: Record<string, string[]>; message?: string }
|
||||||
|
|
||||||
|
export async function returnAssignment(
|
||||||
|
formData: ReturnAssignmentFormType,
|
||||||
|
): Promise<ReturnAssignmentActionResult> {
|
||||||
const { dictionary } = await getI18n()
|
const { dictionary } = await getI18n()
|
||||||
const copy = dictionary.inventory.assignments
|
const copy = dictionary.inventory.assignments
|
||||||
const userId = await getAuthenticatedUserId()
|
const userId = await getAuthenticatedUserId()
|
||||||
|
|
||||||
|
const validatedFields = buildReturnAssignmentSchema(copy.schema).safeParse(
|
||||||
|
formData,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!validatedFields.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: flattenError(validatedFields.error).fieldErrors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await returnAssignmentUseCase({
|
const result = await returnAssignmentUseCase({
|
||||||
id: formData.id,
|
id: validatedFields.data.id,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
returns: validatedFields.data.returns,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
@@ -2,53 +2,219 @@
|
|||||||
|
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useTransition } from "react"
|
import { useState, useTransition } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { returnAssignment } from "@/actions/assignment.actions"
|
import { returnAssignment } from "@/actions/assignment.actions"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
import type { ReturnAssignmentFormType } from "@/schemas/assignment.schema"
|
import type { ReturnAssignmentFormType } from "@/schemas/assignment.schema"
|
||||||
|
|
||||||
|
type PartialReturnCopy = {
|
||||||
|
title: string
|
||||||
|
quantity: string
|
||||||
|
quantityPlaceholder: string
|
||||||
|
notes: string
|
||||||
|
notesPlaceholder: string
|
||||||
|
submit: string
|
||||||
|
cancel: string
|
||||||
|
maxQuantity: string
|
||||||
|
errorConcurrent: string
|
||||||
|
errorGeneric: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPartialReturnCopy: PartialReturnCopy = {
|
||||||
|
title: "Devolver artículo",
|
||||||
|
quantity: "Cantidad",
|
||||||
|
quantityPlaceholder: "1",
|
||||||
|
notes: "Notas",
|
||||||
|
notesPlaceholder: "Notas opcionales",
|
||||||
|
submit: "Devolver",
|
||||||
|
cancel: "Cancelar",
|
||||||
|
maxQuantity: "Máximo: {max}",
|
||||||
|
errorConcurrent:
|
||||||
|
"La devolución fue modificada por otro usuario. Recarga e inténtalo de nuevo.",
|
||||||
|
errorGeneric: "Error al procesar la devolución",
|
||||||
|
}
|
||||||
|
|
||||||
export default function ReturnButton({
|
export default function ReturnButton({
|
||||||
assignmentId,
|
assignmentId,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
assignmentLineId,
|
||||||
|
remainingQuantity,
|
||||||
|
copy = defaultPartialReturnCopy,
|
||||||
}: {
|
}: {
|
||||||
assignmentId: string
|
assignmentId: string
|
||||||
ariaLabel: string
|
ariaLabel: string
|
||||||
|
assignmentLineId?: string
|
||||||
|
remainingQuantity?: number
|
||||||
|
copy?: PartialReturnCopy
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [quantity, setQuantity] = useState(1)
|
||||||
|
const [notes, setNotes] = useState("")
|
||||||
|
const [errorKey, setErrorKey] = useState<
|
||||||
|
"errorConcurrent" | "errorGeneric" | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const isQuantityMode =
|
||||||
|
assignmentLineId !== undefined && remainingQuantity !== undefined
|
||||||
|
const isOverMax = isQuantityMode && quantity > remainingQuantity
|
||||||
|
const canSubmit = isQuantityMode
|
||||||
|
? quantity >= 1 && quantity <= remainingQuantity && !isPending
|
||||||
|
: !isPending
|
||||||
|
|
||||||
|
const handleOpenChange = (nextOpen: boolean) => {
|
||||||
|
setOpen(nextOpen)
|
||||||
|
if (!nextOpen) {
|
||||||
|
setQuantity(1)
|
||||||
|
setNotes("")
|
||||||
|
setErrorKey(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!canSubmit) return
|
||||||
|
|
||||||
|
setErrorKey(null)
|
||||||
|
|
||||||
|
const formData: ReturnAssignmentFormType = isQuantityMode
|
||||||
|
? {
|
||||||
|
id: assignmentId,
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
assignmentLineId,
|
||||||
|
quantity,
|
||||||
|
notes: notes.trim() || undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: { id: assignmentId }
|
||||||
|
|
||||||
const handleReturn = (formData: ReturnAssignmentFormType) => {
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const response = await returnAssignment(formData)
|
const response = await returnAssignment(formData)
|
||||||
|
|
||||||
if (!response.success && response.errors?.id) {
|
if (response.success) {
|
||||||
toast.error(response.errors.id[0])
|
setOpen(false)
|
||||||
|
setQuantity(1)
|
||||||
|
setNotes("")
|
||||||
|
toast.success(response.message)
|
||||||
|
router.refresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.success) {
|
if (response.errors?.error?.includes("errorConcurrent")) {
|
||||||
toast.success(response.message)
|
setErrorKey("errorConcurrent")
|
||||||
router.refresh()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.message ?? "Unknown error")
|
setErrorKey("errorGeneric")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={() => handleReturn({ id: assignmentId })} className="w-full">
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<input type="hidden" name="id" value={assignmentId} />
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="button"
|
||||||
className="btn btn-error"
|
className="btn btn-error"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{copy.title}</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{copy.title}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
{isQuantityMode && (
|
||||||
|
<>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor={`quantity-${assignmentId}`}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
{copy.quantity}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id={`quantity-${assignmentId}`}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={remainingQuantity}
|
||||||
|
value={quantity}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuantity(Number(event.target.value))
|
||||||
|
}
|
||||||
|
placeholder={copy.quantityPlaceholder}
|
||||||
|
aria-invalid={isOverMax || undefined}
|
||||||
|
/>
|
||||||
|
{isOverMax && (
|
||||||
|
<p className="text-destructive text-sm">
|
||||||
|
{copy.maxQuantity.replace(
|
||||||
|
"{max}",
|
||||||
|
String(remainingQuantity),
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor={`notes-${assignmentId}`}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
{copy.notes}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id={`notes-${assignmentId}`}
|
||||||
|
value={notes}
|
||||||
|
onChange={(event) => setNotes(event.target.value)}
|
||||||
|
placeholder={copy.notesPlaceholder}
|
||||||
|
className="min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{errorKey && (
|
||||||
|
<p className="text-destructive text-sm" role="alert">
|
||||||
|
{copy[errorKey]}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{copy.cancel}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={!canSubmit}>
|
||||||
|
{copy.submit}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { en } from "@/i18n/dictionaries/en"
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
revalidatePath: vi.fn(),
|
||||||
|
getI18n: vi.fn(),
|
||||||
|
getAuthenticatedUserId: vi.fn(),
|
||||||
|
returnAssignmentUseCase: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("next/cache", () => ({
|
||||||
|
revalidatePath: mocks.revalidatePath,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/i18n/server", () => ({
|
||||||
|
getI18n: mocks.getI18n,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/services/auth.service", () => ({
|
||||||
|
getAuthenticatedUserId: mocks.getAuthenticatedUserId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/use-cases/assignment.use-cases", () => ({
|
||||||
|
returnAssignmentUseCase: mocks.returnAssignmentUseCase,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { returnAssignment } from "@/actions/assignment.actions"
|
||||||
|
|
||||||
|
describe("returnAssignment action", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" })
|
||||||
|
mocks.getAuthenticatedUserId.mockResolvedValue("user-1")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns validation errors for a missing assignment id", async () => {
|
||||||
|
const result = await returnAssignment({ id: "" })
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [en.inventory.assignments.schema.idRequired],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(mocks.returnAssignmentUseCase).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("forwards returns to the use case and revalidates on success", async () => {
|
||||||
|
mocks.returnAssignmentUseCase.mockResolvedValue({ success: true })
|
||||||
|
|
||||||
|
const result = await returnAssignment({
|
||||||
|
id: "assignment-1",
|
||||||
|
returns: [
|
||||||
|
{ assignmentLineId: "line-1", quantity: 2, notes: "Slightly damaged" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true as const,
|
||||||
|
message: en.inventory.assignments.actions.returnSuccess,
|
||||||
|
})
|
||||||
|
expect(mocks.returnAssignmentUseCase).toHaveBeenCalledWith({
|
||||||
|
id: "assignment-1",
|
||||||
|
actorId: "user-1",
|
||||||
|
returns: [
|
||||||
|
{ assignmentLineId: "line-1", quantity: 2, notes: "Slightly damaged" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/assignments")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("surfaces use-case errors and skips revalidation on failure", async () => {
|
||||||
|
mocks.returnAssignmentUseCase.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
errors: { error: ["errorConcurrent"] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await returnAssignment({
|
||||||
|
id: "assignment-1",
|
||||||
|
returns: [{ assignmentLineId: "line-1", quantity: 2 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false as const,
|
||||||
|
errors: { error: ["errorConcurrent"] },
|
||||||
|
message: en.inventory.assignments.actions.returnFailure,
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves the legacy full-return shortcut without returns", async () => {
|
||||||
|
mocks.returnAssignmentUseCase.mockResolvedValue({ success: true })
|
||||||
|
|
||||||
|
const result = await returnAssignment({ id: "assignment-1" })
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(mocks.returnAssignmentUseCase).toHaveBeenCalledWith({
|
||||||
|
id: "assignment-1",
|
||||||
|
actorId: "user-1",
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/assignments")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import "@testing-library/jest-dom/vitest"
|
||||||
|
import { cleanup, render, screen } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
returnAssignment: vi.fn(),
|
||||||
|
refresh: vi.fn(),
|
||||||
|
toastError: vi.fn(),
|
||||||
|
toastSuccess: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/actions/assignment.actions", () => ({
|
||||||
|
returnAssignment: mocks.returnAssignment,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => ({ refresh: mocks.refresh }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("sonner", () => ({
|
||||||
|
toast: { error: mocks.toastError, success: mocks.toastSuccess },
|
||||||
|
}))
|
||||||
|
|
||||||
|
import ReturnButton from "@/app/(dashboard)/assignments/_components/return.button"
|
||||||
|
|
||||||
|
const mockCopy = {
|
||||||
|
title: "Devolver artículo",
|
||||||
|
quantity: "Cantidad",
|
||||||
|
quantityPlaceholder: "1",
|
||||||
|
notes: "Notas",
|
||||||
|
notesPlaceholder: "Notas opcionales",
|
||||||
|
submit: "Devolver",
|
||||||
|
cancel: "Cancelar",
|
||||||
|
maxQuantity: "Máximo: {max}",
|
||||||
|
errorConcurrent:
|
||||||
|
"La devolución fue modificada por otro usuario. Recarga e inténtalo de nuevo.",
|
||||||
|
errorGeneric: "Error al procesar la devolución",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ReturnButton", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("opens the dialog and submits a partial return", async () => {
|
||||||
|
mocks.returnAssignment.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
message: "Returned",
|
||||||
|
})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ReturnButton
|
||||||
|
assignmentId="assignment-1"
|
||||||
|
ariaLabel="Return assignment"
|
||||||
|
assignmentLineId="line-1"
|
||||||
|
remainingQuantity={5}
|
||||||
|
copy={mockCopy}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Return assignment" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByRole("dialog")).toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
screen.getByRole("heading", { name: mockCopy.title }),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
|
||||||
|
const quantityInput = screen.getByRole("spinbutton", {
|
||||||
|
name: mockCopy.quantity,
|
||||||
|
})
|
||||||
|
await userEvent.clear(quantityInput)
|
||||||
|
await userEvent.type(quantityInput, "2")
|
||||||
|
|
||||||
|
const notesInput = screen.getByRole("textbox", { name: mockCopy.notes })
|
||||||
|
await userEvent.type(notesInput, "Slightly damaged")
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: mockCopy.submit }))
|
||||||
|
|
||||||
|
expect(mocks.returnAssignment).toHaveBeenCalledWith({
|
||||||
|
id: "assignment-1",
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
assignmentLineId: "line-1",
|
||||||
|
quantity: 2,
|
||||||
|
notes: "Slightly damaged",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith("Returned")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("disables submit and shows max quantity when quantity exceeds remaining", async () => {
|
||||||
|
render(
|
||||||
|
<ReturnButton
|
||||||
|
assignmentId="assignment-1"
|
||||||
|
ariaLabel="Return assignment"
|
||||||
|
assignmentLineId="line-1"
|
||||||
|
remainingQuantity={3}
|
||||||
|
copy={mockCopy}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Return assignment" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
const quantityInput = screen.getByRole("spinbutton", {
|
||||||
|
name: mockCopy.quantity,
|
||||||
|
})
|
||||||
|
await userEvent.clear(quantityInput)
|
||||||
|
await userEvent.type(quantityInput, "4")
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: mockCopy.submit })).toBeDisabled()
|
||||||
|
expect(screen.getByText("Máximo: 3")).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shows concurrency error when the action returns one", async () => {
|
||||||
|
mocks.returnAssignment.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
errors: { error: ["errorConcurrent"] },
|
||||||
|
message: "Error returning assignment",
|
||||||
|
})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ReturnButton
|
||||||
|
assignmentId="assignment-1"
|
||||||
|
ariaLabel="Return assignment"
|
||||||
|
assignmentLineId="line-1"
|
||||||
|
remainingQuantity={5}
|
||||||
|
copy={mockCopy}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Return assignment" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
const quantityInput = screen.getByRole("spinbutton", {
|
||||||
|
name: mockCopy.quantity,
|
||||||
|
})
|
||||||
|
await userEvent.clear(quantityInput)
|
||||||
|
await userEvent.type(quantityInput, "2")
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: mockCopy.submit }))
|
||||||
|
|
||||||
|
expect(screen.getByText(mockCopy.errorConcurrent)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("opens confirm dialog and submits legacy full return without quantity input", async () => {
|
||||||
|
mocks.returnAssignment.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
message: "Returned",
|
||||||
|
})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ReturnButton
|
||||||
|
assignmentId="assignment-1"
|
||||||
|
ariaLabel="Return assignment"
|
||||||
|
copy={mockCopy}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Return assignment" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByRole("spinbutton")).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: mockCopy.submit }))
|
||||||
|
|
||||||
|
expect(mocks.returnAssignment).toHaveBeenCalledWith({ id: "assignment-1" })
|
||||||
|
})
|
||||||
|
})
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: "node",
|
environment: "node",
|
||||||
include: ["tests/**/*.test.ts"],
|
include: ["tests/**/*.test.ts", "tests/**/*.test.tsx"],
|
||||||
watch: false,
|
watch: false,
|
||||||
globals: false,
|
globals: false,
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user