@@ -0,0 +1,133 @@
|
||||
// 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",
|
||||
]),
|
||||
}))
|
||||
Reference in New Issue
Block a user