179 lines
7.4 KiB
TypeScript
179 lines
7.4 KiB
TypeScript
// 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 { crmModule } from "../module.js"
|
|
import { crmPage } from "../ui/pages.js"
|
|
|
|
const catalogues = buildRegistry([tenancyModule, crmModule]).catalogues
|
|
const i18n = createI18n("en", { en: { crm: catalogues.en?.crm ?? {}, app: { failed: "That did not work: {detail}" } } })
|
|
const slugs = { org: "acme", workspace: "ws", project: "p" }
|
|
const EVERYTHING: readonly PermissionKey[] = ["crm.party.read", "crm.party.write"]
|
|
|
|
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>,
|
|
)
|
|
|
|
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>) =>
|
|
crmPage.load({ slugs, segments, search: {}, caller: callerOf(answers), i18n, 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 } = crmPage
|
|
return inProvider(
|
|
<View slugs={slugs} segments={segments} locale="en" data={JSON.parse(JSON.stringify(data))} />,
|
|
permissions,
|
|
)
|
|
}
|
|
|
|
const party = {
|
|
partyId: "p-1",
|
|
kind: "organization",
|
|
displayName: "Acme Supplies",
|
|
legalName: "Acme Supplies UAB",
|
|
country: "LT",
|
|
status: "active",
|
|
linked: false,
|
|
}
|
|
|
|
describe("the crm page", () => {
|
|
it("lists the parties, with the form for whoever may add one", async () => {
|
|
const answers = { "crm.party.list": { rows: [party] } }
|
|
const html = await render([], answers)
|
|
expect(html).toContain(">Customers</h1>")
|
|
// The row's name is its link: the kit's row link (`kit-row-link`).
|
|
const link = html.match(/<a\b[^>]*>Acme Supplies<\/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/crm/p-1"')
|
|
expect(link).toContain('aria-label="Open Acme Supplies"')
|
|
expect(html).toContain('aria-label="Parties"')
|
|
expect(html).toContain("New party")
|
|
expect(await render([], answers, ["crm.party.read"])).not.toContain("New party")
|
|
})
|
|
|
|
it("shows a party's contact points and the form that adds one", async () => {
|
|
const html = await render(["p-1"], {
|
|
"crm.party.get": {
|
|
party: { ...party, linked: true },
|
|
contacts: [
|
|
{
|
|
contactId: "c1",
|
|
kind: "email",
|
|
value: "orders@acme.example",
|
|
purposes: ["invoice_email"],
|
|
isPrimary: true,
|
|
erased: false,
|
|
},
|
|
{ contactId: "c2", kind: "phone", value: "", purposes: [], isPrimary: false, erased: true },
|
|
],
|
|
},
|
|
})
|
|
expect(html).toContain(">Acme Supplies</h1>")
|
|
expect(html).toContain("Linked to a member's account")
|
|
expect(html).toContain("2 contact points")
|
|
expect(html).toContain('aria-label="Contact points"')
|
|
for (const text of ["E-mail", "orders@acme.example", "Invoices by e-mail", "Yes"]) expect(html).toContain(text)
|
|
expect(html).toContain("Erased")
|
|
expect(html).toContain("Add a contact point")
|
|
})
|
|
|
|
it("shows the kit's not-enabled state when the organization has not enabled the service", async () => {
|
|
const refused = new AppError({
|
|
type: "/problems/service_not_enabled",
|
|
title: "Service prerequisite unavailable",
|
|
status: 404,
|
|
code: "service_not_enabled",
|
|
detail: "Service admission refused: not_enabled.",
|
|
})
|
|
const html = await render([], { "crm.party.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 sentences", async () => {
|
|
const lt = createI18n("lt", { lt: { crm: catalogues.lt?.crm ?? {} } })
|
|
const silent = new AppError({
|
|
type: "/problems/business_unavailable",
|
|
title: "Refused",
|
|
status: 503,
|
|
code: "business_unavailable",
|
|
})
|
|
const data = await crmPage.load({
|
|
slugs,
|
|
segments: [],
|
|
search: {},
|
|
caller: callerOf({ "crm.party.list": silent }),
|
|
i18n: lt,
|
|
container: new Container(),
|
|
})
|
|
expect(data).toMatchObject({
|
|
kind: "state",
|
|
notice: { code: "business_unavailable", text: "Paslauga neatsakė. Bandykite vėliau." },
|
|
})
|
|
const list = await crmPage.load({
|
|
slugs,
|
|
segments: [],
|
|
search: {},
|
|
caller: callerOf({ "crm.party.list": { rows: [] } }),
|
|
i18n: lt,
|
|
container: new Container(),
|
|
})
|
|
expect(list.kind === "list" && list.form.refusals.business_stale_revision).toBe(
|
|
"Kažkas tai pakeitė, kol jūs dirbote. Atnaujinkite ir bandykite dar kartą.",
|
|
)
|
|
})
|
|
|
|
it("leaves a party 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(["p-2"], { "crm.party.get": missing })).rejects.toMatchObject({
|
|
problem: { code: "business_not_found" },
|
|
})
|
|
await expect(render(["p-1", "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.crm?.()).resolves.toBe(crmPage)
|
|
})
|
|
})
|
|
|
|
// Explicit synthetic admission: module-local fixtures exercise the selected module.
|
|
vi.mock("../../inclusion.json", () => ({ default: { schemaVersion: 1, selected: ["crm"], everAdopted: ["crm"] } }))
|