feat(card): Konfiguration, Geometrie und Bildausschnitt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
05a735993b
commit
1a4ed0d595
6 files changed
+858
No files matched your search
@@ -0,0 +1,182 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
DEFAULT_CROP,
|
||||
NETWORK_IDS,
|
||||
TONE_COLOR,
|
||||
parseConfig,
|
||||
serializeConfig,
|
||||
type CardConfig,
|
||||
} from './config';
|
||||
|
||||
const custom: CardConfig = {
|
||||
v: 1,
|
||||
template: 'classic',
|
||||
network: 'jcb',
|
||||
details: 'dark',
|
||||
brand: 'apple',
|
||||
crop: { x: -120.5, y: 33, zoom: 2.5 },
|
||||
};
|
||||
|
||||
describe('Standardwerte', () => {
|
||||
it('DEFAULT_CONFIG hat die vereinbarten Werte', () => {
|
||||
expect(DEFAULT_CONFIG).toEqual({
|
||||
v: 1,
|
||||
template: 'signature',
|
||||
network: 'visa-auto',
|
||||
details: 'light',
|
||||
brand: 'aircards-symbol',
|
||||
crop: { x: 0, y: 0, zoom: 1 },
|
||||
});
|
||||
expect(DEFAULT_CROP).toEqual({ x: 0, y: 0, zoom: 1 });
|
||||
expect(DEFAULT_CONFIG.crop).toEqual(DEFAULT_CROP);
|
||||
});
|
||||
|
||||
it('DEFAULT_CONFIG.crop ist ein eigenes Objekt, nicht DEFAULT_CROP', () => {
|
||||
expect(DEFAULT_CONFIG.crop).not.toBe(DEFAULT_CROP);
|
||||
});
|
||||
|
||||
it('NETWORK_IDS: 18 Logos in der vereinbarten Reihenfolge', () => {
|
||||
expect(NETWORK_IDS.length).toBe(18);
|
||||
expect([...NETWORK_IDS]).toEqual([
|
||||
'visa-auto', 'visa-blue', 'visa-navy', 'visa-silver',
|
||||
'mc-color', 'mc-silver', 'mc-wordmark', 'mc-silver-text', 'mc-classic',
|
||||
'up-color', 'up-silver', 'up-white-frame', 'up-english',
|
||||
'amex-blue', 'amex-outline', 'amex-select',
|
||||
'jcb', 'discover',
|
||||
]);
|
||||
expect(new Set(NETWORK_IDS).size).toBe(18);
|
||||
});
|
||||
|
||||
it('TONE_COLOR: Hell weiß, Dunkel fast schwarz', () => {
|
||||
expect(TONE_COLOR).toEqual({ light: '#FFFFFF', dark: '#111111' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseConfig / serializeConfig', () => {
|
||||
it('Rundlauf ergibt dieselbe Konfiguration', () => {
|
||||
expect(parseConfig(serializeConfig(custom))).toEqual(custom);
|
||||
expect(parseConfig(serializeConfig(DEFAULT_CONFIG))).toEqual(DEFAULT_CONFIG);
|
||||
});
|
||||
|
||||
it('null, undefined, leer und kaputtes JSON ergeben die Standardkonfiguration', () => {
|
||||
expect(parseConfig(null)).toEqual(DEFAULT_CONFIG);
|
||||
expect(parseConfig(undefined)).toEqual(DEFAULT_CONFIG);
|
||||
expect(parseConfig('')).toEqual(DEFAULT_CONFIG);
|
||||
expect(parseConfig('kaputt{')).toEqual(DEFAULT_CONFIG);
|
||||
});
|
||||
|
||||
it('JSON, das kein Objekt ist, ergibt die Standardkonfiguration', () => {
|
||||
for (const raw of ['null', '42', '"signature"', '[]', 'true']) {
|
||||
expect(parseConfig(raw)).toEqual(DEFAULT_CONFIG);
|
||||
}
|
||||
});
|
||||
|
||||
it('unbekanntes Netzwerk setzt nur dieses Feld auf den Standard', () => {
|
||||
const raw = JSON.stringify({ ...custom, network: 'paypal' });
|
||||
expect(parseConfig(raw)).toEqual({ ...custom, network: DEFAULT_CONFIG.network });
|
||||
});
|
||||
|
||||
it('jedes Feld wird einzeln validiert', () => {
|
||||
const raw = JSON.stringify({ ...custom, template: 'foo', details: 'blue', brand: 5 });
|
||||
expect(parseConfig(raw)).toEqual({
|
||||
...custom,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
details: DEFAULT_CONFIG.details,
|
||||
brand: DEFAULT_CONFIG.brand,
|
||||
});
|
||||
});
|
||||
|
||||
it('Namen aus Object.prototype gelten nicht als gültige Werte', () => {
|
||||
const raw = JSON.stringify({ ...custom, network: 'constructor', template: '__proto__', brand: 'toString' });
|
||||
expect(parseConfig(raw)).toEqual({
|
||||
...custom,
|
||||
network: DEFAULT_CONFIG.network,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
brand: DEFAULT_CONFIG.brand,
|
||||
});
|
||||
});
|
||||
|
||||
it('fehlende Felder bekommen den Standard', () => {
|
||||
expect(parseConfig('{"template":"image"}')).toEqual({ ...DEFAULT_CONFIG, template: 'image' });
|
||||
expect(parseConfig('{}')).toEqual(DEFAULT_CONFIG);
|
||||
});
|
||||
|
||||
it('zoom wird auf 1 bis 4 begrenzt', () => {
|
||||
expect(parseConfig(JSON.stringify({ ...custom, crop: { x: 0, y: 0, zoom: 99 } })).crop.zoom).toBe(4);
|
||||
expect(parseConfig(JSON.stringify({ ...custom, crop: { x: 0, y: 0, zoom: 0.2 } })).crop.zoom).toBe(1);
|
||||
expect(parseConfig(JSON.stringify({ ...custom, crop: { x: 0, y: 0, zoom: -3 } })).crop.zoom).toBe(1);
|
||||
});
|
||||
|
||||
it('nicht endliche oder falsch typisierte Zahlen fallen auf den Standard des Feldes zurück', () => {
|
||||
// NaN und Infinity werden von JSON.stringify zu null
|
||||
const nan = serializeConfig({ ...custom, crop: { x: NaN, y: Infinity, zoom: NaN } });
|
||||
expect(parseConfig(nan).crop).toEqual({ x: 0, y: 0, zoom: 1 });
|
||||
const text = parseConfig('{"crop":{"x":"12","y":"NaN","zoom":"3"}}');
|
||||
expect(text.crop).toEqual({ x: 0, y: 0, zoom: 1 });
|
||||
});
|
||||
|
||||
it('crop.x NaN setzt nur x zurück', () => {
|
||||
const raw = serializeConfig({ ...custom, crop: { x: NaN, y: 33, zoom: 2.5 } });
|
||||
expect(parseConfig(raw).crop).toEqual({ x: 0, y: 33, zoom: 2.5 });
|
||||
});
|
||||
|
||||
it('fehlender oder kaputter crop ergibt den Standard-Ausschnitt', () => {
|
||||
expect(parseConfig('{"crop":"abc"}').crop).toEqual(DEFAULT_CROP);
|
||||
expect(parseConfig('{"crop":null}').crop).toEqual(DEFAULT_CROP);
|
||||
expect(parseConfig('{"crop":[1,2,3]}').crop).toEqual(DEFAULT_CROP);
|
||||
expect(parseConfig('{"template":"minimal"}').crop).toEqual(DEFAULT_CROP);
|
||||
});
|
||||
|
||||
it('x und y werden nicht begrenzt (das macht clampCrop mit dem Bild)', () => {
|
||||
const c = parseConfig(JSON.stringify({ ...custom, crop: { x: -9999, y: 5000, zoom: 1 } }));
|
||||
expect(c.crop).toEqual({ x: -9999, y: 5000, zoom: 1 });
|
||||
});
|
||||
|
||||
it('unbekannte Felder werden verworfen', () => {
|
||||
const raw = JSON.stringify({ ...custom, evil: '<script>', crop: { ...custom.crop, extra: 1 } });
|
||||
const c = parseConfig(raw);
|
||||
expect(c).toEqual(custom);
|
||||
expect(Object.keys(c).sort()).toEqual(['brand', 'crop', 'details', 'network', 'template', 'v']);
|
||||
expect(Object.keys(c.crop).sort()).toEqual(['x', 'y', 'zoom']);
|
||||
});
|
||||
|
||||
it('serializeConfig schreibt nur bekannte Felder', () => {
|
||||
const s = serializeConfig({ ...custom, evil: 1 } as CardConfig);
|
||||
expect(s).not.toContain('evil');
|
||||
expect(JSON.parse(s)).toEqual(custom);
|
||||
});
|
||||
|
||||
it('serializeConfig bleibt unter 2000 Zeichen', () => {
|
||||
expect(serializeConfig(DEFAULT_CONFIG).length).toBeLessThan(2000);
|
||||
const worst: CardConfig = {
|
||||
...custom,
|
||||
network: 'mc-silver-text',
|
||||
brand: 'aircards-symbol',
|
||||
crop: { x: -1.2345678901234567e-300, y: -9.87654321098765e300, zoom: 3.3333333333333335 },
|
||||
};
|
||||
expect(serializeConfig(worst).length).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
it('parseConfig liefert immer neue Objekte, die Standardwerte lassen sich nicht verändern', () => {
|
||||
const a = parseConfig(null);
|
||||
const b = parseConfig(null);
|
||||
expect(a).not.toBe(b);
|
||||
expect(a).not.toBe(DEFAULT_CONFIG);
|
||||
expect(a.crop).not.toBe(b.crop);
|
||||
expect(a.crop).not.toBe(DEFAULT_CONFIG.crop);
|
||||
expect(a.crop).not.toBe(DEFAULT_CROP);
|
||||
|
||||
a.crop.x = 500;
|
||||
a.template = 'image';
|
||||
expect(parseConfig(null)).toEqual(DEFAULT_CONFIG);
|
||||
expect(DEFAULT_CONFIG.crop.x).toBe(0);
|
||||
expect(DEFAULT_CONFIG.template).toBe('signature');
|
||||
expect(DEFAULT_CROP).toEqual({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const c = parseConfig('{"template":"image"}');
|
||||
c.crop.zoom = 3;
|
||||
expect(DEFAULT_CROP.zoom).toBe(1);
|
||||
expect(parseConfig('{}').crop.zoom).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user