Files
template-app/test/brand-and-i18n.test.ts
T
2026-11-14 09:56:45 +00:00

134 lines
6.3 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The brand is the only source of colour (decision AT-D11), and every string a page shows is a
// message in both locales (spec §10.4).
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it, vi } from "vitest"
import { appCatalogues } from "../apps/web/src/ui/messages"
import { appBrand, BRAND_TOKEN_NAMES, brandTokenStyle } from "../config/src/brand.js"
const walk = (dir: string, suffix: RegExp): string[] =>
readdirSync(dir).flatMap((name) => {
// Installed packages are not this app's pages, and a linked workspace package would pull its
// own sources (stories, fixtures) into the scan. What a build or the Start plugin writes is not source.
if (["node_modules", "dist", ".tanstack"].includes(name)) return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return walk(path, suffix)
return suffix.test(path) ? [path] : []
})
const appDir = new URL("..", import.meta.url).pathname
describe("the app brand", () => {
it("reads its tokens from @conusai-kit/brand, so the app defines none", () => {
expect(BRAND_TOKEN_NAMES.length).toBeGreaterThan(0)
expect(appBrand.tokens).toEqual({})
expect(brandTokenStyle({ ...appBrand, tokens: { primary: "#123456" } })).toEqual({ "--brand-primary": "#123456" })
})
it("ships no colour literal of its own in the stylesheet", () => {
const css = readFileSync(join(appDir, "apps/web/src/ui/globals.css"), "utf8")
const literals = css.match(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/g) ?? []
expect(literals).toEqual([])
})
it("loads no external stylesheet, font or script", () => {
// The document's head is `src/ui/document.tsx` and the root route's `head`, not only the pages.
const files = walk(join(appDir, "apps/web/src"), /\.(?:tsx|css)$/)
expect(files.some((file) => file.endsWith("src/routes/__root.tsx"))).toBe(true)
expect(files.some((file) => file.endsWith("src/ui/document.tsx"))).toBe(true)
expect(files.some((file) => file.endsWith("src/ui/globals.css"))).toBe(true)
for (const file of files) {
const text = readFileSync(file, "utf8")
expect(text, `${file} reaches outside this app`).not.toMatch(/https?:\/\/(?!127\.0\.0\.1|localhost)/)
}
})
it("has no attribution turned on by default: a customer sells their own product", () => {
expect(appBrand.attribution).toBe(false)
})
})
describe("the message catalogues", () => {
const en = appCatalogues.en ?? {}
const lt = appCatalogues.lt ?? {}
it("hold the same keys in English and in Lithuanian", () => {
for (const [module, messages] of Object.entries(en)) {
expect(Object.keys(lt[module] ?? {}).sort(), `lt is missing keys of ${module}`).toEqual(
Object.keys(messages).sort(),
)
}
})
it("select the Lithuanian plural categories, which English does not have", () => {
expect(lt.workspace?.projectCount).toContain("few {")
expect(en.workspace?.projectCount).not.toContain("few {")
})
it("name no role of their own: a role's one name is the kit's, on every page", () => {
// The invitation form, the invitation page, the members table and the role picker all read
// `builtInRoleName` and `roleName` of @conusai-kit/tenancy/ui, so a member is the same Editor on each.
expect(en.roles).toBeUndefined()
expect(lt.roles).toBeUndefined()
for (const file of ["../apps/web/src/server/org-pages.ts", "../apps/web/src/server/account-pages.ts"])
expect(readFileSync(new URL(file, import.meta.url), "utf8"), file).not.toMatch(/message\(`roles\./)
})
it("hold the messages a page picks by a value rather than by name: sign-in errors, invitation states", () => {
const keys = [
...["sessionExpired", "noTransaction", "denied", "provider", "failed"].map((code) => `signin.error.${code}`),
...["invalid", "used", "expired", "member", "failed"].map((state) => `invite.error.${state}`),
]
for (const key of keys) {
const [module, ...rest] = key.split(".")
const message = rest.join(".")
expect(en[module as string]?.[message], `en is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `lt is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `${key} is not translated`).not.toBe(en[module as string]?.[message])
}
})
it("select the Lithuanian plural categories of the counts the workspace pages show", () => {
expect(lt.workspace?.projectCount).toContain("few {")
expect(en.workspace?.projectCount).not.toContain("few {")
})
it("hold every message the pages ask for, in both locales", () => {
// Every label a page shows is `i18n.message("<key>")`, so the keys the pages use and the keys
// the catalogues hold are comparable -- and a renamed message fails here rather than
// rendering its own key to a customer. A loader builds a page's labels on the server (F10), so the
// keys are in `.ts` files as well as `.tsx`, often through a local `message` over `i18n.message`.
const used = new Set<string>()
for (const file of walk(join(appDir, "apps/web"), /\.tsx?$/)) {
for (const [, key] of readFileSync(file, "utf8").matchAll(/\bmessage\("([^"]+)"/g)) used.add(key as string)
}
// The shell's labels, read from `shellData()`: a walk that missed loaders would stop finding these.
expect(used).toContain("app.signIn")
expect(used).toContain("boundary.notFoundTitle")
// And the pages' own: `src/server/account-pages.ts` and `src/server/org-pages.ts`.
expect(used).toContain("app.organizations")
expect(used).toContain("workspace.create")
expect(used.size).toBeGreaterThan(10)
for (const key of [...used].sort()) {
const [module, ...rest] = key.split(".")
const message = rest.join(".")
expect(en[module as string]?.[message], `en is missing ${key}`).toBeTruthy()
expect(lt[module as string]?.[message], `lt is missing ${key}`).toBeTruthy()
}
})
})
// This journey requires explicit optional-module admission.
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
...(await import("./support/native-readiness.js")).selectedModules([
"documents",
"knowledge",
"agent",
"guides",
"support",
]),
}))