feat(assignments): partial return action and ReturnButton modal

This commit is contained in:
2026-06-25 21:18:34 +02:00
parent de40e0bf73
commit 2c03cd4d66
5 changed files with 494 additions and 24 deletions
@@ -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" })
})
})