070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
136 lines
5.9 KiB
TypeScript
136 lines
5.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import type { Brand } from '@open-design/contracts';
|
|
|
|
import { brandToMemoryEntries, reflowBrandToMemory } from '../src/brands/memory.js';
|
|
import { listMemoryEntries, readMemoryEntry, writeMemoryConfig } from '../src/memory.js';
|
|
|
|
// A realistic two-stack editorial brand (modeled on the Economist brand pack):
|
|
// seven roles, serif display + sans body, a documented voice, square-corner
|
|
// layout posture, and logo usage notes.
|
|
const ECONOMIST: Brand = {
|
|
name: 'The Economist',
|
|
tagline: 'A severe contest between intelligence and ignorance.',
|
|
description: 'A weekly international newspaper.',
|
|
sourceUrl: 'https://economist.com/',
|
|
logo: {
|
|
primary: 'logos/service-google.png',
|
|
alternates: ['logos/service-ddg.ico'],
|
|
notes:
|
|
"White serif 'E' knocked out of an Economist Red square. Never recolor the red field, never outline it.",
|
|
},
|
|
colors: [
|
|
{ role: 'background', hex: '#ffffff', oklch: '', name: 'Paper White', usage: 'page background' },
|
|
{ role: 'surface', hex: '#f2f2f2', oklch: '', name: 'London 95', usage: 'panels' },
|
|
{ role: 'foreground', hex: '#0d0d0d', oklch: '', name: 'London 5', usage: 'ink' },
|
|
{ role: 'muted', hex: '#595959', oklch: '', name: 'London 35', usage: 'captions' },
|
|
{ role: 'border', hex: '#d9d9d9', oklch: '', name: 'London 85', usage: 'hairlines' },
|
|
{ role: 'accent', hex: '#e3120b', oklch: '', name: 'Economist Red', usage: 'masthead' },
|
|
{ role: 'accent-secondary', hex: '#2e45b8', oklch: '', name: 'Chicago 30', usage: 'links' },
|
|
],
|
|
typography: {
|
|
display: { family: 'Milo TE', fallbacks: ['Georgia', 'Times New Roman', 'serif'], weights: [400, 700] },
|
|
body: { family: 'Econ Sans', fallbacks: ['Helvetica Neue', 'Arial', 'sans-serif'], weights: [400, 700] },
|
|
},
|
|
voice: {
|
|
adjectives: ['authoritative', 'concise', 'witty'],
|
|
tone: 'A single unsigned house voice that argues a position.',
|
|
messagingPillars: ['Analysis over breaking news', 'A global outlook'],
|
|
vocabulary: { use: ['this newspaper', 'leader', 'briefing'], avoid: ['content', 'hot take', 'synergy'] },
|
|
},
|
|
imagery: {
|
|
style: 'Editorial illustration and photojournalism in service of an argument.',
|
|
subjects: ['world leaders', 'markets'],
|
|
treatment: 'Bold conceptual illustration.',
|
|
avoid: ['generic stock-photo handshakes', 'emoji and clip-art'],
|
|
},
|
|
layout: {
|
|
radius: '0px',
|
|
borderWeight: '1px',
|
|
spacing: '8px baseline grid',
|
|
postureRules: ['Square corners everywhere', 'Hairline rules separate stories'],
|
|
},
|
|
};
|
|
|
|
describe('brandToMemoryEntries', () => {
|
|
it('derives a coherent visual / voice / rule set with a stable slug', () => {
|
|
const entries = brandToMemoryEntries(ECONOMIST);
|
|
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
|
|
// One user visual-direction, one reference voice, four rules.
|
|
expect(byId.has('user_brand_the_economist_visual')).toBe(true);
|
|
expect(byId.has('reference_brand_the_economist_voice')).toBe(true);
|
|
expect(byId.has('rule_brand_the_economist_palette')).toBe(true);
|
|
expect(byId.has('rule_brand_the_economist_type')).toBe(true);
|
|
expect(byId.has('rule_brand_the_economist_layout')).toBe(true);
|
|
expect(byId.has('rule_brand_the_economist_logo')).toBe(true);
|
|
|
|
const visual = byId.get('user_brand_the_economist_visual')!;
|
|
expect(visual.type).toBe('user');
|
|
expect(visual.body).toContain('#e3120b');
|
|
expect(visual.body).toContain('Milo TE');
|
|
|
|
// Palette rule reads as an enforceable Assertion/Check the self-verify pass
|
|
// can score against, and lists every allowed hex.
|
|
const palette = byId.get('rule_brand_the_economist_palette')!;
|
|
expect(palette.type).toBe('rule');
|
|
expect(palette.body).toMatch(/^Assertion:/);
|
|
expect(palette.body).toContain('Check:');
|
|
expect(palette.body).toContain('#2e45b8');
|
|
|
|
const logo = byId.get('rule_brand_the_economist_logo')!;
|
|
expect(logo.body).toContain('Never recolor the red field');
|
|
});
|
|
|
|
it('skips facets the brand does not carry', () => {
|
|
const bare: Brand = {
|
|
...ECONOMIST,
|
|
voice: { adjectives: [], tone: '', messagingPillars: [], vocabulary: { use: [], avoid: [] } },
|
|
imagery: { style: '', subjects: [], treatment: '', avoid: [] },
|
|
logo: { primary: null, alternates: [], notes: '' },
|
|
};
|
|
const ids = brandToMemoryEntries(bare).map((e) => e.id);
|
|
expect(ids).not.toContain('reference_brand_the_economist_voice');
|
|
expect(ids).not.toContain('rule_brand_the_economist_logo');
|
|
// Visual + palette + type + layout still derive from the colors/type/layout.
|
|
expect(ids).toContain('user_brand_the_economist_visual');
|
|
expect(ids).toContain('rule_brand_the_economist_palette');
|
|
});
|
|
});
|
|
|
|
describe('reflowBrandToMemory', () => {
|
|
let dataDir: string;
|
|
|
|
beforeEach(() => {
|
|
dataDir = mkdtempSync(path.join(os.tmpdir(), 'od-brand-memory-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('writes brand entries through the memory store and links them in the index', async () => {
|
|
const result = await reflowBrandToMemory(dataDir, ECONOMIST);
|
|
expect(result.written.length).toBeGreaterThanOrEqual(5);
|
|
|
|
const stored = await listMemoryEntries(dataDir);
|
|
const ids = stored.map((e) => e.id);
|
|
expect(ids).toContain('user_brand_the_economist_visual');
|
|
expect(ids).toContain('rule_brand_the_economist_palette');
|
|
|
|
const palette = await readMemoryEntry(dataDir, 'rule_brand_the_economist_palette');
|
|
expect(palette?.type).toBe('rule');
|
|
});
|
|
|
|
it('writes nothing when memory is disabled', async () => {
|
|
await writeMemoryConfig(dataDir, { enabled: false });
|
|
const result = await reflowBrandToMemory(dataDir, ECONOMIST);
|
|
expect(result.skipped).toBe('memory-disabled');
|
|
expect(result.written).toEqual([]);
|
|
expect(await listMemoryEntries(dataDir)).toEqual([]);
|
|
});
|
|
});
|