feat(quick-260907-fgv-01): Markenbausteine — Icons, Farbkonstanten, TesseraLogo-Komponente
- favicon.svg und apple-touch-icon.png byteidentisch nach src/app/ kopiert (dateibasierte Next.js-Metadaten fuer den Browser-Tab) - brand.ts: einzige Stelle im Web-Quellcode fuer die drei Markenfarben (BRAND_YELLOW, BRAND_OLIVE, BRAND_PLATE), aus favicon.svg uebernommen - TesseraLogo-Komponente mit den Varianten mark/horizontal/stacked, als JSX-SVG von Hand gesetzt (kein dangerouslySetInnerHTML), Kontur-Klasse fuer die dunkle Platte per plateOutline-Eigenschaft schaltbar - common.brand.logoAlt in de.json und en.json ergaenzt, Schluesselparitaet per Test abgesichert Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { BRAND_YELLOW } from './brand';
|
||||
import { TesseraLogo } from './tessera-logo';
|
||||
import de from '@/messages/de.json';
|
||||
import en from '@/messages/en.json';
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: (ns: string) => (key: string) => {
|
||||
const map: Record<string, Record<string, string>> = {
|
||||
common: {
|
||||
appName: 'Tessera',
|
||||
'brand.logoAlt': 'Tessera Logo',
|
||||
},
|
||||
};
|
||||
return map[ns]?.[key] ?? key;
|
||||
},
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
function flattenKeys(obj: unknown, prefix = ''): string[] {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return [prefix];
|
||||
}
|
||||
return Object.entries(obj as Record<string, unknown>).flatMap(([key, value]) =>
|
||||
flattenKeys(value, prefix ? `${prefix}.${key}` : key),
|
||||
);
|
||||
}
|
||||
|
||||
describe('TesseraLogo', () => {
|
||||
it('variant "mark" renders an img-role element with the translated accessible name and no visible wordmark', () => {
|
||||
render(<TesseraLogo variant="mark" />);
|
||||
|
||||
const mark = screen.getByRole('img', { name: 'Tessera Logo' });
|
||||
expect(mark).toBeInTheDocument();
|
||||
expect(screen.queryByText('Tessera')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('variant "horizontal" renders the img-role mark and the visible wordmark text', () => {
|
||||
render(<TesseraLogo variant="horizontal" />);
|
||||
|
||||
expect(screen.getByRole('img', { name: 'Tessera Logo' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Tessera')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('variant "stacked" renders the img-role mark and the visible wordmark text', () => {
|
||||
render(<TesseraLogo variant="stacked" />);
|
||||
|
||||
expect(screen.getByRole('img', { name: 'Tessera Logo' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Tessera')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('enables the dark-mode plate outline class by default', () => {
|
||||
render(<TesseraLogo variant="mark" />);
|
||||
|
||||
const mark = screen.getByRole('img', { name: 'Tessera Logo' });
|
||||
const plate = mark.querySelector('rect');
|
||||
expect(plate?.getAttribute('class')).toContain('dark:stroke-white/25');
|
||||
});
|
||||
|
||||
it('omits the dark-mode plate outline class when plateOutline is false', () => {
|
||||
render(<TesseraLogo variant="mark" plateOutline={false} />);
|
||||
|
||||
const mark = screen.getByRole('img', { name: 'Tessera Logo' });
|
||||
const plate = mark.querySelector('rect');
|
||||
expect(plate?.getAttribute('class')).not.toContain('dark:stroke-white/25');
|
||||
});
|
||||
|
||||
it('renders exactly five tiles, exactly one in the brand yellow and exactly one rotated', () => {
|
||||
render(<TesseraLogo variant="mark" />);
|
||||
|
||||
const mark = screen.getByRole('img', { name: 'Tessera Logo' });
|
||||
const tiles = Array.from(mark.querySelectorAll('g rect'));
|
||||
expect(tiles).toHaveLength(5);
|
||||
|
||||
const yellowTiles = tiles.filter((tile) => tile.getAttribute('fill') === BRAND_YELLOW);
|
||||
expect(yellowTiles).toHaveLength(1);
|
||||
|
||||
const rotatedTiles = tiles.filter((tile) => tile.hasAttribute('transform'));
|
||||
expect(rotatedTiles).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('common.brand i18n namespace — de/en key parity', () => {
|
||||
it('both locales define common.brand.logoAlt', () => {
|
||||
expect(de.common).toHaveProperty('brand.logoAlt');
|
||||
expect(en.common).toHaveProperty('brand.logoAlt');
|
||||
});
|
||||
|
||||
it('has an identical (recursively-flattened) key set under common.brand in de and en', () => {
|
||||
const deKeys = flattenKeys((de.common as { brand: unknown }).brand).sort();
|
||||
const enKeys = flattenKeys((en.common as { brand: unknown }).brand).sort();
|
||||
|
||||
expect(deKeys).toEqual(enKeys);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user