@@ -0,0 +1,203 @@
|
||||
// The pages and components, rendered on the server the way the shell renders them: the platform's
|
||||
// refusals are the page's own states, every control has a name, every table a caption and scoped
|
||||
// headers.
|
||||
|
||||
import { AppError, buildRegistry, Container, type TransportCaller } from "@conusai-kit/core"
|
||||
import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n"
|
||||
import { tenancyModule } from "@conusai-kit/tenancy"
|
||||
import { createDataClient, DataProvider, type PermissionKey, PermissionsProvider } from "@conusai-kit/ui/data"
|
||||
import { UiLabelsProvider } from "@conusai-kit/ui/primitives"
|
||||
import type { ReactNode } from "react"
|
||||
import { renderToStaticMarkup as renderMarkup } from "react-dom/server"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { modulePages } from "../../pages.js"
|
||||
import { inventoryModule } from "../module.js"
|
||||
import { inventoryPage } from "../ui/pages.js"
|
||||
|
||||
const catalogues = buildRegistry([tenancyModule, inventoryModule]).catalogues
|
||||
const i18n = createI18n("en", {
|
||||
en: { inventory: catalogues.en?.inventory ?? {}, app: { failed: "That did not work: {detail}" } },
|
||||
})
|
||||
const slugs = { org: "acme", workspace: "ws", project: "p" }
|
||||
const ITEM = "0b5c8e9e-1f2a-4c55-9a77-3f1e2d4c5b6a"
|
||||
const LOCATION = "7d1e2f3a-0000-4000-8000-000000000001"
|
||||
|
||||
const EVERYTHING: readonly PermissionKey[] = ["inventory.item.read", "inventory.item.write", "inventory.stock.reserve"]
|
||||
|
||||
/** Rendered as the shell renders a page: a data client, and the caller's permissions in the page's scope. */
|
||||
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
|
||||
const renderToStaticMarkup = (node: React.ReactNode) =>
|
||||
renderMarkup(<UiLabelsProvider labels={kitStrings}>{node}</UiLabelsProvider>)
|
||||
|
||||
const inProvider = (node: ReactNode, permissions: readonly PermissionKey[] = EVERYTHING) =>
|
||||
renderToStaticMarkup(
|
||||
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
|
||||
<PermissionsProvider scope={slugs} seed={{ scope: slugs, permissions, revision: "1" }}>
|
||||
{node}
|
||||
</PermissionsProvider>
|
||||
</DataProvider>,
|
||||
)
|
||||
|
||||
/** A caller that answers each procedure from `answers`, or throws what is there. */
|
||||
const callerOf = (answers: Record<string, unknown>): TransportCaller =>
|
||||
({
|
||||
call: async (contract: { name: string }) => {
|
||||
const answer = answers[contract.name]
|
||||
if (answer instanceof Error) throw answer
|
||||
return answer
|
||||
},
|
||||
}) as unknown as TransportCaller
|
||||
|
||||
const load = (segments: string[], answers: Record<string, unknown>, reader = i18n) =>
|
||||
inventoryPage.load({
|
||||
slugs,
|
||||
segments,
|
||||
search: {},
|
||||
caller: callerOf(answers),
|
||||
i18n: reader,
|
||||
container: new Container(),
|
||||
})
|
||||
|
||||
/** The page as the shell renders it: the loader's data through JSON, as the browser receives it, then the view. */
|
||||
const render = async (segments: string[], answers: Record<string, unknown>, permissions?: readonly PermissionKey[]) => {
|
||||
const data = await load(segments, answers)
|
||||
expect(JSON.parse(JSON.stringify(data))).toEqual(data)
|
||||
const { View } = inventoryPage
|
||||
return inProvider(
|
||||
<View slugs={slugs} segments={segments} locale="en" data={JSON.parse(JSON.stringify(data))} />,
|
||||
permissions,
|
||||
)
|
||||
}
|
||||
|
||||
const platformRefusal = (status: number, code: string, detail: string) =>
|
||||
new AppError({ type: `/problems/${code}`, title: "Refused", status, code, detail })
|
||||
|
||||
describe("the inventory page", () => {
|
||||
it("lists the items and this project's reservations", async () => {
|
||||
const html = await render([], {
|
||||
"inventory.item.list": {
|
||||
rows: [{ itemId: ITEM, sku: "BOLT-8", name: "Bolt M8", unit: "pcs", description: null, active: true }],
|
||||
},
|
||||
"inventory.reservation.list": {
|
||||
rows: [
|
||||
{
|
||||
reservationId: "r1",
|
||||
itemId: ITEM,
|
||||
locationId: LOCATION,
|
||||
quantity: 3,
|
||||
outstanding: 3,
|
||||
released: 0,
|
||||
issued: 0,
|
||||
status: "open",
|
||||
createdAt: "2026-11-02T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(html).toContain(">Inventory</h1>")
|
||||
const link = html.match(/<a\b[^>]*>BOLT-8<\/a>/)?.[0] ?? ""
|
||||
expect(link).toContain('data-slot="row-link"')
|
||||
expect(link.match(/class="([^"]*)"/)?.[1]?.split(/\s+/)).toContain("kit-row-link")
|
||||
expect(link).toContain(`href="/acme/ws/p/inventory/${ITEM}"`)
|
||||
expect(link).toContain('aria-label="Stock of BOLT-8"')
|
||||
expect(html).toContain('aria-label="Items"')
|
||||
expect(html).toContain('aria-label="Open reservations of this project"')
|
||||
expect(html).toContain("BOLT-8 Bolt M8")
|
||||
expect(html).toContain("SKU")
|
||||
expect(html).toContain('role="search"')
|
||||
})
|
||||
|
||||
it("shows an item's stock by location, and the form that reserves it for whoever may", async () => {
|
||||
const answers = {
|
||||
"inventory.item.availability": {
|
||||
item: { itemId: ITEM, sku: "BOLT-8", name: "Bolt M8", unit: "pcs", description: null, active: true },
|
||||
availability: {
|
||||
itemId: ITEM,
|
||||
unit: "pcs",
|
||||
onHand: 10,
|
||||
reserved: 3,
|
||||
available: 7,
|
||||
locations: [{ locationId: LOCATION, onHand: 10, reserved: 3, available: 7 }],
|
||||
},
|
||||
},
|
||||
}
|
||||
const html = await render([ITEM], answers)
|
||||
expect(html).toContain("Counted in pcs")
|
||||
expect(html).toContain("1 location with stock")
|
||||
expect(html).toContain('aria-label="Stock by location"')
|
||||
expect(html).toContain("7d1e2f3a")
|
||||
expect(html).toContain("Reserve for this project")
|
||||
expect(html).toContain("7d1e2f3a (7)")
|
||||
const reader = await render([ITEM], answers, ["inventory.item.read"])
|
||||
expect(reader).not.toContain("Reserve for this project")
|
||||
})
|
||||
|
||||
it("offers the item form only to a member who may write items", async () => {
|
||||
const answers = { "inventory.item.list": { rows: [] }, "inventory.reservation.list": { rows: [] } }
|
||||
expect(await render([], answers)).toContain("New item")
|
||||
const reader = await render([], answers, ["inventory.item.read"])
|
||||
expect(reader).not.toContain("New item")
|
||||
expect(reader).toContain("No items yet.")
|
||||
})
|
||||
|
||||
it("shows the kit's not-enabled state when the organization has not enabled the service", async () => {
|
||||
const refused = platformRefusal(404, "service_not_enabled", "Service admission refused: not_enabled.")
|
||||
const html = await render([], { "inventory.item.list": refused, "inventory.reservation.list": refused })
|
||||
expect(html).toContain('data-state="service_not_enabled"')
|
||||
expect(html).toContain("This service is not enabled for your organization.")
|
||||
expect(html).not.toContain('role="grid"')
|
||||
})
|
||||
|
||||
it("tells a Lithuanian reader in the kit's Lithuanian sentence", async () => {
|
||||
const lt = createI18n("lt", { lt: { inventory: catalogues.lt?.inventory ?? {} } })
|
||||
const refused = platformRefusal(404, "service_not_enabled", "Service admission refused: not_enabled.")
|
||||
const data = await load([], { "inventory.item.list": refused, "inventory.reservation.list": refused }, lt)
|
||||
expect(data).toMatchObject({
|
||||
kind: "state",
|
||||
notice: { code: "service_not_enabled", text: "Ši paslauga jūsų organizacijai neįjungta." },
|
||||
})
|
||||
const answers = { "inventory.item.list": { rows: [] }, "inventory.reservation.list": { rows: [] } }
|
||||
const list = await load([], answers, lt)
|
||||
expect(list.kind === "list" && list.itemForm.refusals.business_duplicate).toBe("Toks įrašas jau yra.")
|
||||
expect(list.kind === "list" && list.reservationList.refusals.business_reservation_state).toBe(
|
||||
"Šio rezervavimo dabartinėje būsenoje pakeisti negalima.",
|
||||
)
|
||||
})
|
||||
|
||||
it("shows a missing permission and a silent service in the kit's words", async () => {
|
||||
const scoped = platformRefusal(403, "service_permission_missing", "Service admission refused: permission_missing.")
|
||||
expect(await render([ITEM], { "inventory.item.availability": scoped })).toContain(
|
||||
"This app is not allowed to use this service. Ask an administrator to grant it.",
|
||||
)
|
||||
const silent = platformRefusal(503, "business_unavailable", "The service did not answer. Try again later.")
|
||||
const html = await render([ITEM], { "inventory.item.availability": silent })
|
||||
expect(html).toContain('data-state="business_unavailable"')
|
||||
expect(html).toContain("The service did not answer. Try again later.")
|
||||
})
|
||||
|
||||
it("leaves any other refusal, and a path it does not have, to the shell", async () => {
|
||||
await expect(
|
||||
render([ITEM], {
|
||||
"inventory.item.availability": new AppError({
|
||||
type: "/problems/not_found",
|
||||
title: "Not found",
|
||||
status: 404,
|
||||
code: "not_found",
|
||||
}),
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
problem: { code: "not_found" },
|
||||
})
|
||||
await expect(render(["not-an-id"], {})).rejects.toMatchObject({ problem: { status: 404 } })
|
||||
await expect(render([ITEM, "more"], {})).rejects.toMatchObject({ problem: { status: 404 } })
|
||||
})
|
||||
|
||||
it("is listed for the shell", async () => {
|
||||
await expect(modulePages.inventory?.()).resolves.toBe(inventoryPage)
|
||||
})
|
||||
})
|
||||
|
||||
// Explicit synthetic admission: module-local fixtures exercise the selected module.
|
||||
vi.mock("../../inclusion.json", () => ({
|
||||
default: { schemaVersion: 1, selected: ["inventory"], everAdopted: ["inventory"] },
|
||||
}))
|
||||
Reference in New Issue
Block a user