Files
template-app/test/brand-and-i18n.test.ts

102 lines
4.5 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 } 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: string): 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.
if (name === "node_modules" || name === ".next") return []
const path = join(dir, name)
if (statSync(path).isDirectory()) return walk(path, suffix)
return path.endsWith(suffix) ? [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/app/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", () => {
for (const file of [...walk(join(appDir, "apps/web/app"), ".tsx"), join(appDir, "apps/web/app/globals.css")]) {
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.app?.memberCount).toContain("few {")
expect(en.app?.memberCount).not.toContain("few {")
})
it("hold the messages a page picks by a value rather than by name: roles, sign-in errors, invitation states", () => {
const keys = [
...["viewer", "member", "admin", "owner"].map((role) => `roles.${role}`),
...["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.
const used = new Set<string>()
for (const file of walk(join(appDir, "apps/web"), ".tsx")) {
for (const [, key] of readFileSync(file, "utf8").matchAll(/i18n\.message\("([^"]+)"/g)) used.add(key as string)
}
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()
}
})
})