Files
vending-machine-game/tests/content.test.js
T
HamedandClaude Opus 5.5 8dbaf3d97e refactor: centralize brand, company info, assets and Persian strings
Phase 1 of turning the game into the company landing page.

- Add src/config/company.js with name, tagline, contact, about and the
  product line, plus a brand palette built around #00a693 (contrast
  checked); placeholders are marked TODO(company).
- Add src/config/assets.js listing every loaded file with its expected
  format; placeholders are marked TODO(asset).
- Move all Persian interface text, dialogue, missions and scenery text
  into src/i18n/fa.js; components and game code read from it.
- Expose the palette as CSS variables (--color-primary and shades) and
  Tailwind colours, and use it for 3D materials and map drawings.
- Fix the arrival line that still called the manager's car a white
  pickup.
- Add tests that block hard-coded Persian text and low-contrast colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:13:00 +03:30

75 lines
2.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { COMPANY } from '../src/config/company.js';
import { strings } from '../src/i18n/fa.js';
const SRC = new URL('../src/', import.meta.url).pathname;
// Persian text may only live in the strings file, the company config, map data and the list of
// machine sites (place names and addresses are map content, not interface text).
const ALLOWED = ['i18n/', 'config/', 'data/', 'constants/locations.js'];
const PERSIAN = /[؀-ۿ]/;
// Persian digits used for number formatting, not text.
const DIGITS_ONLY = /^[۰-۹]$/;
/**
* Lists source files below a folder.
* @param {string} dir - Folder to walk.
* @returns {string[]} File paths.
*/
function sourceFiles(dir) {
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
if (statSync(path).isDirectory()) return sourceFiles(path);
return /\.(js|jsx)$/.test(name) && !/\.test\./.test(name) ? [path] : [];
});
}
/**
* WCAG contrast ratio between two #rrggbb colours.
* @param {string} a - First colour.
* @param {string} b - Second colour.
* @returns {number} Ratio from 1 to 21.
*/
function contrast(a, b) {
const luminance = (hex) => {
const [r, g, bl] = [1, 3, 5]
.map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
.map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
};
const [high, low] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (high + 0.05) / (low + 0.05);
}
test('Interface text lives in the strings file, not in components or game code', () => {
const offenders = sourceFiles(SRC)
.map((path) => relative(SRC, path))
.filter((path) => !ALLOWED.some((allowed) => path.startsWith(allowed)))
.flatMap((path) =>
readFileSync(join(SRC, path), 'utf8')
.split('\n')
.map((line, i) => [line, i + 1])
.filter(([line]) => [...line].some((ch) => PERSIAN.test(ch) && !DIGITS_ONLY.test(ch)))
.map(([, n]) => `${path}:${n}`),
);
assert.deepEqual(offenders, []);
});
test('Brand palette keeps readable contrast', () => {
const c = COMPANY.colors;
assert.equal(c.primary, '#00a693');
assert.ok(contrast(c.primary, c.surface) >= 4.5, 'primary accents on dark surfaces');
assert.ok(contrast(c.onPrimary, c.primary) >= 4.5, 'text on primary buttons');
assert.ok(contrast(c.text, c.surface) >= 7, 'body text');
assert.ok(contrast(c.textMuted, c.surface) >= 4.5, 'secondary text');
});
test('Strings that take values build complete sentences', () => {
assert.equal(strings.prompts.getIn('بی‌ام‌و'), 'F · سوار بی‌ام‌و شو');
assert.equal(strings.units.hetoman('۵۰۰'), '۵۰۰ هتومان');
assert.ok(strings.briefing.subtitle.includes(COMPANY.shortName));
assert.equal(strings.ranks.length, 6);
});