@@ -0,0 +1,189 @@
|
||||
// The pages and components, rendered the way the shell renders them, from the loader's data: the platform's
|
||||
// refusals are the page's own states, a control is offered to whoever may use it, every table has 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 { catalogModule } from "../module.js"
|
||||
import { catalogPage } from "../ui/pages.js"
|
||||
|
||||
const catalogues = buildRegistry([tenancyModule, catalogModule]).catalogues
|
||||
const i18n = createI18n("en", {
|
||||
en: { catalog: catalogues.en?.catalog ?? {}, app: { failed: "That did not work: {detail}" } },
|
||||
})
|
||||
const lithuanian = createI18n("lt", {
|
||||
lt: { catalog: catalogues.lt?.catalog ?? {}, app: { failed: "Nepavyko: {detail}" } },
|
||||
})
|
||||
const slugs = { org: "acme", workspace: "ws", project: "p" }
|
||||
const EVERYTHING: readonly PermissionKey[] = ["catalog.product.read", "catalog.product.write"]
|
||||
|
||||
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
|
||||
const renderToStaticMarkup = (node: 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>,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
/** 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 catalogPage.load({
|
||||
slugs,
|
||||
segments,
|
||||
search: {},
|
||||
caller: callerOf(answers),
|
||||
i18n,
|
||||
container: new Container(),
|
||||
})
|
||||
expect(JSON.parse(JSON.stringify(data))).toEqual(data)
|
||||
const { View } = catalogPage
|
||||
return inProvider(
|
||||
<View slugs={slugs} segments={segments} locale="en" data={JSON.parse(JSON.stringify(data))} />,
|
||||
permissions,
|
||||
)
|
||||
}
|
||||
|
||||
const product = {
|
||||
productId: "prd_1",
|
||||
code: "BOLT-M8",
|
||||
name: "Bolt M8",
|
||||
description: "A bolt",
|
||||
baseUnit: "C62",
|
||||
status: "active",
|
||||
groupCode: null,
|
||||
identifiers: [{ kind: "gtin", value: "4006381333931" }],
|
||||
revision: "3",
|
||||
}
|
||||
|
||||
const escaped = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
|
||||
/** Every label of a form names a control the page holds, and every hint is in its control's description. */
|
||||
const expectLabelled = (html: string, labels: number): void => {
|
||||
const targets = [...html.matchAll(/<label[^>]* for="([^"]+)"/g)].map((match) => match[1] ?? "")
|
||||
expect(targets).toHaveLength(labels)
|
||||
for (const id of targets) expect(html).toMatch(new RegExp(`<(?:input|select|button)[^>]* id="${escaped(id)}"`))
|
||||
for (const [, id = ""] of html.matchAll(/<p id="([^"]+)" data-slot="field-description"/g))
|
||||
expect(html).toMatch(new RegExp(`aria-describedby="(?:[^"]* )?${escaped(id)}[ "]`))
|
||||
}
|
||||
|
||||
const applied = {
|
||||
importId: "cim_1",
|
||||
sourceName: "products.csv",
|
||||
format: "csv",
|
||||
status: "applied",
|
||||
revision: "1",
|
||||
created: 2,
|
||||
updated: 1,
|
||||
unchanged: 0,
|
||||
refused: 0,
|
||||
}
|
||||
|
||||
describe("the catalog page", () => {
|
||||
it("lists the products, with the way to the imports", async () => {
|
||||
const html = await render([], { "catalog.product.list": { rows: [product] } })
|
||||
expect(html).toMatch(/<h1\b[^>]*>Products<\/h1>/)
|
||||
expect(html).toMatch(
|
||||
/<a\b(?=[^>]*href="\/acme\/ws\/p\/catalog\/prd_1")(?=[^>]*aria-label="Open Bolt M8")[^>]*>Bolt M8<\/a>/,
|
||||
)
|
||||
expect(html).toMatch(/<table\b[^>]*aria-label="Products"/)
|
||||
expect(html).toMatch(/<th\b[^>]*role="columnheader"[^>]*>Code<\/th>/)
|
||||
expect(html).toContain('href="/acme/ws/p/catalog/imports"')
|
||||
})
|
||||
|
||||
it("shows one product with its identifiers and revision", async () => {
|
||||
const html = await render(["prd_1"], { "catalog.product.get": product })
|
||||
expect(html).toContain("<h2>Bolt M8</h2>")
|
||||
expect(html).toContain("<dd>BOLT-M8</dd>")
|
||||
expect(html).toContain("<dd>3</dd>")
|
||||
expect(html).toContain("4006381333931")
|
||||
expect(html).toContain('href="/acme/ws/p/catalog"')
|
||||
})
|
||||
|
||||
it("lists the imports, with the form for whoever may apply one", async () => {
|
||||
const answers = { "catalog.import.list": { rows: [applied] } }
|
||||
const html = await render(["imports"], answers)
|
||||
expect(html).toMatch(/<h1\b[^>]*>Imports<\/h1>/)
|
||||
expect(html).toMatch(/<th\b[^>]*scope="row"[^>]*>products\.csv<\/th>/)
|
||||
expect(html).toMatch(/<caption\b[^>]*>Imports<\/caption>/)
|
||||
expect(html).toContain('type="file"')
|
||||
expectLabelled(html, 1)
|
||||
expect(html.match(/data-slot="field-description"/g)).toHaveLength(1)
|
||||
expect(await render(["imports"], answers, ["catalog.product.read"])).not.toContain('type="file"')
|
||||
})
|
||||
|
||||
it.each([
|
||||
["service_not_enabled", "This service is not enabled for your organization."],
|
||||
["service_permission_missing", "This app is not allowed to use this service. Ask an administrator to grant it."],
|
||||
["business_unavailable", "The service did not answer. Try again later."],
|
||||
])("shows the kit's state %s", async (code, text) => {
|
||||
const refused = new AppError({ type: `/problems/${code}`, title: "Refused", status: 503, code, detail: "why" })
|
||||
const html = await render([], { "catalog.product.list": refused })
|
||||
expect(html).toContain(`data-state="${code}"`)
|
||||
expect(html).toContain('role="status"')
|
||||
expect(html).toContain(text)
|
||||
expect(html).not.toContain("<table")
|
||||
})
|
||||
|
||||
it("shows the same state in Lithuanian", async () => {
|
||||
const code = "service_not_enabled"
|
||||
const refused = new AppError({ type: `/problems/${code}`, title: "Refused", status: 404, code })
|
||||
const data = await catalogPage.load({
|
||||
slugs,
|
||||
segments: [],
|
||||
search: {},
|
||||
caller: callerOf({ "catalog.product.list": refused }),
|
||||
i18n: lithuanian,
|
||||
container: new Container(),
|
||||
})
|
||||
expect(data).toMatchObject({
|
||||
kind: "state",
|
||||
notice: { code, text: "Ši paslauga jūsų organizacijai neįjungta." },
|
||||
})
|
||||
})
|
||||
|
||||
it("leaves a product that is not there, and a path it does not have, to the shell", async () => {
|
||||
const missing = new AppError({
|
||||
type: "/problems/business_not_found",
|
||||
title: "Not found",
|
||||
status: 404,
|
||||
code: "business_not_found",
|
||||
})
|
||||
await expect(render(["prd_2"], { "catalog.product.get": missing })).rejects.toMatchObject({
|
||||
problem: { code: "business_not_found" },
|
||||
})
|
||||
await expect(render(["prd_1", "x"], {})).rejects.toMatchObject({ problem: { status: 404 } })
|
||||
await expect(render(["imports", "x"], {})).rejects.toMatchObject({ problem: { status: 404 } })
|
||||
await expect(render(["%2Fetc"], {})).rejects.toMatchObject({ problem: { status: 404 } })
|
||||
await expect(render(["%E0%A4%A"], {})).rejects.toMatchObject({ problem: { status: 404 } })
|
||||
})
|
||||
|
||||
it("is listed for the shell", async () => {
|
||||
await expect(modulePages.catalog?.()).resolves.toBe(catalogPage)
|
||||
})
|
||||
})
|
||||
|
||||
// Explicit synthetic admission: module-local fixtures exercise the selected module.
|
||||
vi.mock("../../inclusion.json", () => ({
|
||||
default: { schemaVersion: 1, selected: ["catalog"], everAdopted: ["catalog"] },
|
||||
}))
|
||||
Reference in New Issue
Block a user