import { describe, it, expect } from 'vitest'; import { DEFAULT_CONFIG, DEFAULT_CROP, ID_PATTERN, TONE_COLOR, parseConfig, serializeConfig, type CardConfig, } from './config'; const custom: CardConfig = { v: 1, template: 'classic', logo: 'abc123', details: 'dark', brand: 'aircards-text', 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', logo: null, 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('eine neue Karte hat kein Logo unten rechts', () => { expect(DEFAULT_CONFIG.logo).toBeNull(); }); it('ID_PATTERN: Appwrite-IDs mit höchstens 36 Zeichen und ohne Sonderzeichen am Anfang', () => { for (const ok of ['abc123', 'A', '0', 'a.b_c-d', 'x'.repeat(36)]) expect(ID_PATTERN.test(ok), ok).toBe(true); for (const bad of ['', 'kaputt id', '-abc', '_abc', '.abc', 'ä', 'a/b', 'x'.repeat(37)]) expect(ID_PATTERN.test(bad), bad).toBe(false); }); 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('eine alte Konfiguration mit network und Apple-Logo: network entfällt, Logo oben links fällt auf den Standard', () => { const raw = '{"v":1,"template":"classic","network":"mc-color","brand":"apple","details":"dark"}'; expect(parseConfig(raw)).toEqual({ v: 1, template: 'classic', logo: null, details: 'dark', brand: 'aircards-symbol', crop: { x: 0, y: 0, zoom: 1 }, }); // andere unbekannte Marken ebenso expect(parseConfig('{"brand":"visa"}').brand).toBe('aircards-symbol'); }); it('logo: eine gültige ID bleibt, alles andere ergibt null', () => { expect(parseConfig('{"logo":"abc123"}').logo).toBe('abc123'); expect(parseConfig(JSON.stringify({ logo: 'x'.repeat(36) })).logo).toBe('x'.repeat(36)); for (const bad of ['kaputt id', '', '-abc', 'x'.repeat(37), 42, true, null, ['abc'], { id: 'abc' }]) { expect(parseConfig(JSON.stringify({ logo: bad })).logo, JSON.stringify(bad)).toBeNull(); } expect(parseConfig('{}').logo).toBeNull(); }); it('das gewählte Logo bleibt nur dem Feld logo zugeordnet: ein altes network wird nie zum Logo', () => { expect(parseConfig('{"network":"abc123"}').logo).toBeNull(); }); 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, template: '__proto__', details: 'constructor', brand: 'toString' }); expect(parseConfig(raw)).toEqual({ ...custom, template: DEFAULT_CONFIG.template, details: DEFAULT_CONFIG.details, 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: '