feat(assets): add asset metadata views and enforce assignment transitions
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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(),
|
||||
createAssetUseCase: vi.fn(),
|
||||
updateAssetUseCase: 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/asset.use-cases", () => ({
|
||||
createAssetUseCase: mocks.createAssetUseCase,
|
||||
updateAssetUseCase: mocks.updateAssetUseCase,
|
||||
}))
|
||||
|
||||
import { createAssetAction, updateAssetAction } from "@/actions/asset.actions"
|
||||
|
||||
describe("asset actions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" })
|
||||
mocks.getAuthenticatedUserId.mockResolvedValue("user-1")
|
||||
})
|
||||
|
||||
it("accepts operational asset fields on create and forwards them to the use case", async () => {
|
||||
mocks.createAssetUseCase.mockResolvedValue({
|
||||
success: true,
|
||||
assetId: "asset-1",
|
||||
})
|
||||
|
||||
const result = await createAssetAction({
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "AVAILABLE",
|
||||
assetTag: "IT-000900",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad P1",
|
||||
purchaseDate: "2026-01-15",
|
||||
purchasePrice: "1400.25",
|
||||
warrantyEndsAt: "2028-01-15",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: en.inventory.assets.actions.createSuccess,
|
||||
})
|
||||
expect(mocks.createAssetUseCase).toHaveBeenCalledWith({
|
||||
actorId: "user-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "AVAILABLE",
|
||||
assetTag: "IT-000900",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad P1",
|
||||
purchaseDate: new Date("2026-01-15T00:00:00.000Z"),
|
||||
purchasePrice: 1400.25,
|
||||
warrantyEndsAt: new Date("2028-01-15T00:00:00.000Z"),
|
||||
})
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/inventory/assets")
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/inventory/items")
|
||||
})
|
||||
|
||||
it("accepts operational asset fields on update and forwards them to the use case", async () => {
|
||||
mocks.updateAssetUseCase.mockResolvedValue({ success: true })
|
||||
|
||||
const result = await updateAssetAction({
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "BROKEN",
|
||||
assetTag: "IT-000901",
|
||||
manufacturer: "Dell",
|
||||
model: "Latitude 7420",
|
||||
purchaseDate: "2026-02-01",
|
||||
purchasePrice: "1499.5",
|
||||
warrantyEndsAt: "2027-02-01",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: en.inventory.assets.actions.updateSuccess,
|
||||
})
|
||||
expect(mocks.updateAssetUseCase).toHaveBeenCalledWith({
|
||||
actorId: "user-1",
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "BROKEN",
|
||||
assetTag: "IT-000901",
|
||||
manufacturer: "Dell",
|
||||
model: "Latitude 7420",
|
||||
purchaseDate: new Date("2026-02-01T00:00:00.000Z"),
|
||||
purchasePrice: 1499.5,
|
||||
warrantyEndsAt: new Date("2027-02-01T00:00:00.000Z"),
|
||||
})
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/inventory/assets")
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/inventory/items")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import { createElement } from "react"
|
||||
import { renderToStaticMarkup } from "react-dom/server"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getI18n: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/i18n/server", () => ({
|
||||
getI18n: mocks.getI18n,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/asset.service", () => ({
|
||||
AssetService: {
|
||||
findById: mocks.findById,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/components/common/pageheader", () => ({
|
||||
default: ({ title }: { title: string }) =>
|
||||
createElement("header", null, title),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/ui/button", () => ({
|
||||
Button: ({ children }: { children: React.ReactNode }) =>
|
||||
createElement("button", null, children),
|
||||
}))
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children }: { href: string; children: React.ReactNode }) =>
|
||||
createElement("a", { href }, children),
|
||||
}))
|
||||
|
||||
describe("asset detail page", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getI18n.mockResolvedValue({
|
||||
dictionary: {
|
||||
inventory: {
|
||||
assets: {
|
||||
detail: {
|
||||
title: "Asset Details",
|
||||
notFound: "Asset not found",
|
||||
actions: { edit: "Edit asset" },
|
||||
labels: {
|
||||
item: "Item",
|
||||
serialNumber: "Serial Number",
|
||||
assetTag: "Asset Tag",
|
||||
manufacturer: "Manufacturer",
|
||||
model: "Model",
|
||||
purchaseDate: "Purchase Date",
|
||||
purchasePrice: "Purchase Price",
|
||||
warrantyEndsAt: "Warranty Ends At",
|
||||
deliveryNote: "Delivery Note",
|
||||
notes: "Notes",
|
||||
status: "Status",
|
||||
person: "Person",
|
||||
},
|
||||
},
|
||||
list: {
|
||||
title: "Assets",
|
||||
addLabel: "Add Asset",
|
||||
empty: "No assets found.",
|
||||
columns: {},
|
||||
actions: {},
|
||||
},
|
||||
new: { title: "New Asset" },
|
||||
edit: { title: "Edit Asset", notFound: "Asset not found" },
|
||||
form: {},
|
||||
status: { AVAILABLE: "Available" },
|
||||
fallback: { unknownStatus: "Unknown status" },
|
||||
actions: {},
|
||||
schema: {},
|
||||
},
|
||||
},
|
||||
common: { submitButton: {} },
|
||||
},
|
||||
locale: "en",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders the asset operational metadata in the detail view", async () => {
|
||||
mocks.findById.mockResolvedValue({
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
assetTag: "IT-000777",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad X1",
|
||||
purchaseDate: new Date("2026-01-15T00:00:00.000Z"),
|
||||
purchasePrice: 1249.99,
|
||||
warrantyEndsAt: new Date("2028-01-15T00:00:00.000Z"),
|
||||
deliveryNote: "DN-1",
|
||||
notes: "Ready",
|
||||
status: "AVAILABLE",
|
||||
item: { name: "Laptop" },
|
||||
assignment: {
|
||||
person: {
|
||||
id: "person-1",
|
||||
firstName: "Ada",
|
||||
lastName: "Lovelace",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { default: AssetDetailPage } = await import(
|
||||
"@/app/(dashboard)/inventory/assets/[assetId]/page"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await AssetDetailPage({ params: Promise.resolve({ assetId: "asset-1" }) }),
|
||||
)
|
||||
|
||||
expect(html).toContain("Asset Details")
|
||||
expect(html).toContain("Asset Tag")
|
||||
expect(html).toContain("IT-000777")
|
||||
expect(html).toContain("Manufacturer")
|
||||
expect(html).toContain("Lenovo")
|
||||
expect(html).toContain("Ada Lovelace")
|
||||
expect(html).toContain("Purchase Price")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createElement } from "react"
|
||||
import { renderToStaticMarkup } from "react-dom/server"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getI18n: vi.fn(),
|
||||
findAllWithItemAndCategory: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/i18n/server", () => ({
|
||||
getI18n: mocks.getI18n,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/asset.service", () => ({
|
||||
AssetService: {
|
||||
findAllWithItemAndCategory: mocks.findAllWithItemAndCategory,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/components/common/pageheader", () => ({
|
||||
default: ({ title, link }: { title: string; link: string }) =>
|
||||
createElement("header", null, title, link),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/common/pagination", () => ({
|
||||
default: ({ totalPages }: { totalPages: number }) =>
|
||||
createElement("div", null, `pages:${totalPages}`),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/ui/button", () => ({
|
||||
Button: ({ children }: { children: React.ReactNode }) =>
|
||||
createElement("button", null, children),
|
||||
}))
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children }: { href: string; children: React.ReactNode }) =>
|
||||
createElement("a", { href }, children),
|
||||
}))
|
||||
|
||||
describe("assets page", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getI18n.mockResolvedValue({
|
||||
dictionary: {
|
||||
inventory: {
|
||||
assets: {
|
||||
list: {
|
||||
title: "Assets",
|
||||
addLabel: "Add Asset",
|
||||
empty: "No assets found.",
|
||||
columns: {
|
||||
item: "Item",
|
||||
category: "Category",
|
||||
serialNumber: "Serial Number",
|
||||
assetTag: "Asset Tag",
|
||||
manufacturer: "Manufacturer",
|
||||
model: "Model",
|
||||
purchaseDate: "Purchase Date",
|
||||
purchasePrice: "Purchase Price",
|
||||
warrantyEndsAt: "Warranty Ends At",
|
||||
status: "Status",
|
||||
actions: "Actions",
|
||||
},
|
||||
actions: { view: "View asset", edit: "Edit asset" },
|
||||
},
|
||||
new: { title: "New Asset" },
|
||||
edit: { title: "Edit Asset", notFound: "Asset not found" },
|
||||
form: {},
|
||||
status: { AVAILABLE: "Available" },
|
||||
fallback: { unknownStatus: "Unknown status" },
|
||||
actions: {},
|
||||
schema: {},
|
||||
detail: {
|
||||
title: "Asset Details",
|
||||
notFound: "Asset not found",
|
||||
labels: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
common: { submitButton: {} },
|
||||
},
|
||||
locale: "en",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders asset operational columns in the list", async () => {
|
||||
mocks.findAllWithItemAndCategory.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: "asset-1",
|
||||
item: { name: "Laptop", category: { name: "Devices" } },
|
||||
serialNumber: "SERIAL-1",
|
||||
assetTag: "IT-000777",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad X1",
|
||||
purchaseDate: new Date("2026-01-15T00:00:00.000Z"),
|
||||
purchasePrice: 1249.99,
|
||||
warrantyEndsAt: new Date("2028-01-15T00:00:00.000Z"),
|
||||
status: "AVAILABLE",
|
||||
},
|
||||
],
|
||||
totalPages: 1,
|
||||
})
|
||||
|
||||
const { default: AssetsPage } = await import(
|
||||
"@/app/(dashboard)/inventory/assets/page"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await AssetsPage({ searchParams: Promise.resolve({}) }),
|
||||
)
|
||||
|
||||
expect(html).toContain("Asset Tag")
|
||||
expect(html).toContain("Manufacturer")
|
||||
expect(html).toContain("Model")
|
||||
expect(html).toContain("IT-000777")
|
||||
expect(html).toContain("ThinkPad X1")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,202 @@
|
||||
import { createElement } from "react"
|
||||
import { renderToStaticMarkup } from "react-dom/server"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useRouter: vi.fn(() => ({ push: vi.fn() })),
|
||||
updateAssetAction: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: mocks.useRouter,
|
||||
}))
|
||||
|
||||
vi.mock("@/actions/asset.actions", () => ({
|
||||
updateAssetAction: mocks.updateAssetAction,
|
||||
}))
|
||||
|
||||
describe("edit asset form", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders the operational asset fields and current values on the edit form", async () => {
|
||||
const { default: EditAssetForm } = await import(
|
||||
"@/app/(dashboard)/inventory/assets/_components/edit.asset.form"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(EditAssetForm, {
|
||||
asset: {
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "AVAILABLE",
|
||||
assetTag: "IT-000777",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad X1",
|
||||
purchaseDate: new Date("2026-01-15T00:00:00.000Z"),
|
||||
purchasePrice: 1249.99,
|
||||
warrantyEndsAt: new Date("2028-01-15T00:00:00.000Z"),
|
||||
deliveryNote: null,
|
||||
notes: null,
|
||||
personId: null,
|
||||
item: null,
|
||||
assignment: null,
|
||||
},
|
||||
items: [{ id: "item-1", name: "Laptop" }],
|
||||
people: [{ id: "person-1", firstName: "Ada", lastName: "Lovelace" }],
|
||||
formCopy: {
|
||||
itemLabel: "Item",
|
||||
itemPlaceholder: "Select an item",
|
||||
serialNumberLabel: "Serial Number",
|
||||
serialNumberPlaceholder: "Serial number",
|
||||
deliveryNoteLabel: "Delivery Note",
|
||||
deliveryNotePlaceholder: "Delivery note",
|
||||
statusLabel: "Status",
|
||||
statusPlaceholder: "Select a status",
|
||||
personLabel: "Person",
|
||||
personPlaceholder: "Select a person",
|
||||
assetTagLabel: "Asset Tag",
|
||||
assetTagPlaceholder: "Asset tag",
|
||||
manufacturerLabel: "Manufacturer",
|
||||
manufacturerPlaceholder: "Manufacturer",
|
||||
modelLabel: "Model",
|
||||
modelPlaceholder: "Model",
|
||||
purchaseDateLabel: "Purchase Date",
|
||||
purchaseDatePlaceholder: "YYYY-MM-DD",
|
||||
purchasePriceLabel: "Purchase Price",
|
||||
purchasePricePlaceholder: "0.00",
|
||||
warrantyEndsAtLabel: "Warranty Ends At",
|
||||
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
|
||||
createSubmit: "Create Asset",
|
||||
updateSubmit: "Update Asset",
|
||||
},
|
||||
schemaCopy: {
|
||||
itemRequired: "Item is required",
|
||||
serialNumberRequired: "Serial number is required",
|
||||
idRequired: "ID is required",
|
||||
statusRequired: "Status is required",
|
||||
invalidCreateStatus: "Status must be Available or Assigned",
|
||||
invalidUpdateStatus: "Invalid status",
|
||||
personRequired: "Person is required",
|
||||
},
|
||||
statusCopy: {
|
||||
AVAILABLE: "Available",
|
||||
ASSIGNED: "Assigned",
|
||||
IN_REPAIR: "In repair",
|
||||
BROKEN: "Broken",
|
||||
LOST: "Lost",
|
||||
STOLEN: "Stolen",
|
||||
DISPOSED: "Disposed",
|
||||
RETIRED: "Retired",
|
||||
},
|
||||
submitButtonCopy: {
|
||||
defaultLabel: "Submit",
|
||||
processing: "Processing",
|
||||
success: "Success",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain("Asset Tag")
|
||||
expect(html).toContain("IT-000777")
|
||||
expect(html).toContain("Manufacturer")
|
||||
expect(html).toContain("Lenovo")
|
||||
expect(html).toContain("Purchase Price")
|
||||
})
|
||||
|
||||
it("exposes every backend-supported update status", async () => {
|
||||
const { default: EditAssetForm } = await import(
|
||||
"@/app/(dashboard)/inventory/assets/_components/edit.asset.form"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(EditAssetForm, {
|
||||
asset: {
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "AVAILABLE",
|
||||
assetTag: null,
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
purchaseDate: null,
|
||||
purchasePrice: null,
|
||||
warrantyEndsAt: null,
|
||||
deliveryNote: null,
|
||||
notes: null,
|
||||
personId: null,
|
||||
item: null,
|
||||
assignment: null,
|
||||
},
|
||||
items: [{ id: "item-1", name: "Laptop" }],
|
||||
people: [],
|
||||
formCopy: {
|
||||
itemLabel: "Item",
|
||||
itemPlaceholder: "Select an item",
|
||||
serialNumberLabel: "Serial Number",
|
||||
serialNumberPlaceholder: "Serial number",
|
||||
deliveryNoteLabel: "Delivery Note",
|
||||
deliveryNotePlaceholder: "Delivery note",
|
||||
statusLabel: "Status",
|
||||
statusPlaceholder: "Select a status",
|
||||
personLabel: "Person",
|
||||
personPlaceholder: "Select a person",
|
||||
assetTagLabel: "Asset Tag",
|
||||
assetTagPlaceholder: "Asset tag",
|
||||
manufacturerLabel: "Manufacturer",
|
||||
manufacturerPlaceholder: "Manufacturer",
|
||||
modelLabel: "Model",
|
||||
modelPlaceholder: "Model",
|
||||
purchaseDateLabel: "Purchase Date",
|
||||
purchaseDatePlaceholder: "YYYY-MM-DD",
|
||||
purchasePriceLabel: "Purchase Price",
|
||||
purchasePricePlaceholder: "0.00",
|
||||
warrantyEndsAtLabel: "Warranty Ends At",
|
||||
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
|
||||
createSubmit: "Create Asset",
|
||||
updateSubmit: "Update Asset",
|
||||
},
|
||||
schemaCopy: {
|
||||
itemRequired: "Item is required",
|
||||
serialNumberRequired: "Serial number is required",
|
||||
idRequired: "ID is required",
|
||||
statusRequired: "Status is required",
|
||||
invalidCreateStatus: "Status must be Available or Assigned",
|
||||
invalidUpdateStatus: "Invalid status",
|
||||
personRequired: "Person is required",
|
||||
},
|
||||
statusCopy: {
|
||||
AVAILABLE: "Available",
|
||||
ASSIGNED: "Assigned",
|
||||
IN_REPAIR: "In repair",
|
||||
BROKEN: "Broken",
|
||||
LOST: "Lost",
|
||||
STOLEN: "Stolen",
|
||||
DISPOSED: "Disposed",
|
||||
RETIRED: "Retired",
|
||||
},
|
||||
submitButtonCopy: {
|
||||
defaultLabel: "Submit",
|
||||
processing: "Processing",
|
||||
success: "Success",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
for (const status of [
|
||||
"AVAILABLE",
|
||||
"ASSIGNED",
|
||||
"IN_REPAIR",
|
||||
"BROKEN",
|
||||
"LOST",
|
||||
"STOLEN",
|
||||
"DISPOSED",
|
||||
"RETIRED",
|
||||
]) {
|
||||
expect(html).toContain(`value="${status}"`)
|
||||
}
|
||||
expect(html).not.toContain('value="RESERVED"')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,160 @@
|
||||
import { createElement } from "react"
|
||||
import { renderToStaticMarkup } from "react-dom/server"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useRouter: vi.fn(() => ({ push: vi.fn() })),
|
||||
createAssetAction: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: mocks.useRouter,
|
||||
}))
|
||||
|
||||
vi.mock("@/actions/asset.actions", () => ({
|
||||
createAssetAction: mocks.createAssetAction,
|
||||
}))
|
||||
|
||||
describe("new asset form", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders the operational asset fields on the create form", async () => {
|
||||
const { default: NewAssetForm } = await import(
|
||||
"@/app/(dashboard)/inventory/assets/_components/new.asset.form"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(NewAssetForm, {
|
||||
items: [{ id: "item-1", name: "Laptop" }],
|
||||
people: [{ id: "person-1", firstName: "Ada", lastName: "Lovelace" }],
|
||||
formCopy: {
|
||||
itemLabel: "Item",
|
||||
itemPlaceholder: "Select an item",
|
||||
serialNumberLabel: "Serial Number",
|
||||
serialNumberPlaceholder: "Serial number",
|
||||
deliveryNoteLabel: "Delivery Note",
|
||||
deliveryNotePlaceholder: "Delivery note",
|
||||
statusLabel: "Status",
|
||||
statusPlaceholder: "Select a status",
|
||||
personLabel: "Person",
|
||||
personPlaceholder: "Select a person",
|
||||
assetTagLabel: "Asset Tag",
|
||||
assetTagPlaceholder: "Asset tag",
|
||||
manufacturerLabel: "Manufacturer",
|
||||
manufacturerPlaceholder: "Manufacturer",
|
||||
modelLabel: "Model",
|
||||
modelPlaceholder: "Model",
|
||||
purchaseDateLabel: "Purchase Date",
|
||||
purchaseDatePlaceholder: "YYYY-MM-DD",
|
||||
purchasePriceLabel: "Purchase Price",
|
||||
purchasePricePlaceholder: "0.00",
|
||||
warrantyEndsAtLabel: "Warranty Ends At",
|
||||
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
|
||||
createSubmit: "Create Asset",
|
||||
updateSubmit: "Update Asset",
|
||||
},
|
||||
schemaCopy: {
|
||||
itemRequired: "Item is required",
|
||||
serialNumberRequired: "Serial number is required",
|
||||
idRequired: "ID is required",
|
||||
statusRequired: "Status is required",
|
||||
invalidCreateStatus: "Status must be Available or Assigned",
|
||||
invalidUpdateStatus: "Invalid status",
|
||||
personRequired: "Person is required",
|
||||
},
|
||||
statusCopy: {
|
||||
AVAILABLE: "Available",
|
||||
ASSIGNED: "Assigned",
|
||||
IN_REPAIR: "In repair",
|
||||
BROKEN: "Broken",
|
||||
LOST: "Lost",
|
||||
STOLEN: "Stolen",
|
||||
DISPOSED: "Disposed",
|
||||
RETIRED: "Retired",
|
||||
},
|
||||
submitButtonCopy: {
|
||||
defaultLabel: "Submit",
|
||||
processing: "Processing",
|
||||
success: "Success",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain("Asset Tag")
|
||||
expect(html).toContain("Manufacturer")
|
||||
expect(html).toContain("Model")
|
||||
expect(html).toContain("Purchase Date")
|
||||
expect(html).toContain("Purchase Price")
|
||||
expect(html).toContain("Warranty Ends At")
|
||||
})
|
||||
|
||||
it("only exposes create-supported asset statuses", async () => {
|
||||
const { default: NewAssetForm } = await import(
|
||||
"@/app/(dashboard)/inventory/assets/_components/new.asset.form"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(NewAssetForm, {
|
||||
items: [{ id: "item-1", name: "Laptop" }],
|
||||
people: [],
|
||||
formCopy: {
|
||||
itemLabel: "Item",
|
||||
itemPlaceholder: "Select an item",
|
||||
serialNumberLabel: "Serial Number",
|
||||
serialNumberPlaceholder: "Serial number",
|
||||
deliveryNoteLabel: "Delivery Note",
|
||||
deliveryNotePlaceholder: "Delivery note",
|
||||
statusLabel: "Status",
|
||||
statusPlaceholder: "Select a status",
|
||||
personLabel: "Person",
|
||||
personPlaceholder: "Select a person",
|
||||
assetTagLabel: "Asset Tag",
|
||||
assetTagPlaceholder: "Asset tag",
|
||||
manufacturerLabel: "Manufacturer",
|
||||
manufacturerPlaceholder: "Manufacturer",
|
||||
modelLabel: "Model",
|
||||
modelPlaceholder: "Model",
|
||||
purchaseDateLabel: "Purchase Date",
|
||||
purchaseDatePlaceholder: "YYYY-MM-DD",
|
||||
purchasePriceLabel: "Purchase Price",
|
||||
purchasePricePlaceholder: "0.00",
|
||||
warrantyEndsAtLabel: "Warranty Ends At",
|
||||
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
|
||||
createSubmit: "Create Asset",
|
||||
updateSubmit: "Update Asset",
|
||||
},
|
||||
schemaCopy: {
|
||||
itemRequired: "Item is required",
|
||||
serialNumberRequired: "Serial number is required",
|
||||
idRequired: "ID is required",
|
||||
statusRequired: "Status is required",
|
||||
invalidCreateStatus: "Status must be Available or Assigned",
|
||||
invalidUpdateStatus: "Invalid status",
|
||||
personRequired: "Person is required",
|
||||
},
|
||||
statusCopy: {
|
||||
AVAILABLE: "Available",
|
||||
ASSIGNED: "Assigned",
|
||||
IN_REPAIR: "In repair",
|
||||
BROKEN: "Broken",
|
||||
LOST: "Lost",
|
||||
STOLEN: "Stolen",
|
||||
DISPOSED: "Disposed",
|
||||
RETIRED: "Retired",
|
||||
},
|
||||
submitButtonCopy: {
|
||||
defaultLabel: "Submit",
|
||||
processing: "Processing",
|
||||
success: "Success",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('value="AVAILABLE"')
|
||||
expect(html).toContain('value="ASSIGNED"')
|
||||
expect(html).not.toContain('value="IN_REPAIR"')
|
||||
expect(html).not.toContain('value="BROKEN"')
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ const schemaCopy = {
|
||||
statusRequired: "El estado es obligatorio",
|
||||
invalidCreateStatus: "El estado inicial no es válido",
|
||||
invalidUpdateStatus: "El estado no es válido",
|
||||
personRequired: "La persona es obligatoria",
|
||||
}
|
||||
|
||||
describe("asset schema localization", () => {
|
||||
@@ -75,6 +76,65 @@ describe("asset schema localization", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("requires person when create or update status is assigned", () => {
|
||||
const createResult = buildCreateAssetSchema(schemaCopy).safeParse({
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "ASSIGNED",
|
||||
})
|
||||
|
||||
expect(createResult.success).toBe(false)
|
||||
if (!createResult.success) {
|
||||
expect(createResult.error.flatten().fieldErrors.personId).toContain(
|
||||
schemaCopy.personRequired,
|
||||
)
|
||||
}
|
||||
|
||||
const updateResult = buildUpdateAssetSchema(schemaCopy).safeParse({
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "ASSIGNED",
|
||||
})
|
||||
|
||||
expect(updateResult.success).toBe(false)
|
||||
if (!updateResult.success) {
|
||||
expect(updateResult.error.flatten().fieldErrors.personId).toContain(
|
||||
schemaCopy.personRequired,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("aligns create and update status options with the asset status contract", () => {
|
||||
expect(
|
||||
buildCreateAssetSchema(schemaCopy).safeParse({
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "IN_REPAIR",
|
||||
}).success,
|
||||
).toBe(false)
|
||||
|
||||
for (const status of ["LOST", "RETIRED"] as const) {
|
||||
expect(
|
||||
buildUpdateAssetSchema(schemaCopy).safeParse({
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status,
|
||||
}).success,
|
||||
).toBe(true)
|
||||
}
|
||||
|
||||
expect(
|
||||
buildUpdateAssetSchema(schemaCopy).safeParse({
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-1",
|
||||
status: "RESERVED",
|
||||
}).success,
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("keeps optional asset fields optional", () => {
|
||||
const result = buildCreateAssetSchema(schemaCopy).safeParse({
|
||||
itemId: "item-1",
|
||||
@@ -84,4 +144,63 @@ describe("asset schema localization", () => {
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts operational asset fields on create and update payloads", () => {
|
||||
const createResult = buildCreateAssetSchema(schemaCopy).safeParse({
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-2",
|
||||
status: "AVAILABLE",
|
||||
assetTag: "IT-000123",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad T14",
|
||||
purchaseDate: "2026-01-15",
|
||||
purchasePrice: "1249.99",
|
||||
warrantyEndsAt: "2028-01-15",
|
||||
})
|
||||
|
||||
expect(createResult.success).toBe(true)
|
||||
if (createResult.success) {
|
||||
expect(createResult.data).toMatchObject({
|
||||
assetTag: "IT-000123",
|
||||
manufacturer: "Lenovo",
|
||||
model: "ThinkPad T14",
|
||||
purchasePrice: 1249.99,
|
||||
})
|
||||
expect(createResult.data.purchaseDate?.toISOString()).toBe(
|
||||
"2026-01-15T00:00:00.000Z",
|
||||
)
|
||||
expect(createResult.data.warrantyEndsAt?.toISOString()).toBe(
|
||||
"2028-01-15T00:00:00.000Z",
|
||||
)
|
||||
}
|
||||
|
||||
const updateResult = buildUpdateAssetSchema(schemaCopy).safeParse({
|
||||
id: "asset-1",
|
||||
itemId: "item-1",
|
||||
serialNumber: "SERIAL-2",
|
||||
status: "BROKEN",
|
||||
assetTag: "IT-000124",
|
||||
manufacturer: "Dell",
|
||||
model: "Latitude 7420",
|
||||
purchaseDate: "2026-02-01",
|
||||
purchasePrice: "1499.5",
|
||||
warrantyEndsAt: "2027-02-01",
|
||||
})
|
||||
|
||||
expect(updateResult.success).toBe(true)
|
||||
if (updateResult.success) {
|
||||
expect(updateResult.data).toMatchObject({
|
||||
assetTag: "IT-000124",
|
||||
manufacturer: "Dell",
|
||||
model: "Latitude 7420",
|
||||
purchasePrice: 1499.5,
|
||||
})
|
||||
expect(updateResult.data.purchaseDate?.toISOString()).toBe(
|
||||
"2026-02-01T00:00:00.000Z",
|
||||
)
|
||||
expect(updateResult.data.warrantyEndsAt?.toISOString()).toBe(
|
||||
"2027-02-01T00:00:00.000Z",
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user