test(card): loadMarkImage mit Stubs testen, Verweise prüfen, Amex-Umriss freistellen
loadMarkImage: Cache je (id, tone), eingefärbter Blob mit eigener Größe, Freigabe der Blob-URL bei load und error, erneuter Versuch nach Fehler. Neuer Test: jedes url(#...) zeigt auf eine id derselben SVG-Datei. amex-outline: Rahmen nach außen verschoben, berührt "A" und "S" nicht mehr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb6b4ac59d
commit
a610df31dc
2 files changed
+135
-4
No files matched your search
+134
-3
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest';
|
||||
import { NETWORK_IDS } from './config';
|
||||
import favicon from '../../public/favicon.svg?raw';
|
||||
import {
|
||||
@@ -128,6 +128,20 @@ describe('SVG-Inhalt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Verweise innerhalb der Dateien', () => {
|
||||
it.each(ALL.map((m) => [m.id, m] as const))('%s: jedes url(#…) zeigt auf eine id derselben Datei', (_id, m) => {
|
||||
const defined = new Set([...m.svg.matchAll(/\bid="([^"]+)"/g)].map((h) => h[1]));
|
||||
const refs = [...m.svg.matchAll(/url\(\s*['"]?#([^)'"\s]+)/g)].map((h) => h[1]);
|
||||
for (const ref of refs) expect(defined.has(ref), `${ref} fehlt in ${m.id}`).toBe(true);
|
||||
});
|
||||
|
||||
it('die Silber-/Verlaufs-Marken verwenden überhaupt Verweise', () => {
|
||||
for (const id of ['visa-silver', 'mc-silver', 'mc-silver-text', 'up-silver', 'jcb', 'discover']) {
|
||||
expect(getNetworkMark(id as never).svg, id).toMatch(/url\(#/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxH', () => {
|
||||
const h = (id: string) => NETWORK_MARKS.find((m) => m.id === id)!.maxH;
|
||||
|
||||
@@ -159,7 +173,124 @@ describe('Favicon', () => {
|
||||
});
|
||||
|
||||
describe('loadMarkImage', () => {
|
||||
it('ist eine Funktion', () => {
|
||||
expect(typeof loadMarkImage).toBe('function');
|
||||
type Mode = 'load' | 'error';
|
||||
|
||||
/** Ersatz für `Image`: feuert onload/onerror beim Setzen von `src` (je nach `mode`). */
|
||||
const env = { mode: 'load' as Mode, images: [] as FakeImage[] };
|
||||
class FakeImage {
|
||||
onload: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
#src = '';
|
||||
get src() {
|
||||
return this.#src;
|
||||
}
|
||||
set src(value: string) {
|
||||
this.#src = value;
|
||||
env.images.push(this);
|
||||
queueMicrotask(() => (env.mode === 'load' ? this.onload?.() : this.onerror?.()));
|
||||
}
|
||||
}
|
||||
|
||||
const blobs: Blob[] = [];
|
||||
const revoked: string[] = [];
|
||||
const realCreate = URL.createObjectURL;
|
||||
const realRevoke = URL.revokeObjectURL;
|
||||
|
||||
beforeEach(() => {
|
||||
env.mode = 'load';
|
||||
env.images.length = 0;
|
||||
blobs.length = 0;
|
||||
revoked.length = 0;
|
||||
URL.createObjectURL = vi.fn((blob: Blob) => {
|
||||
blobs.push(blob);
|
||||
return `blob:fake/${blobs.length}`;
|
||||
});
|
||||
URL.revokeObjectURL = vi.fn((url: string) => {
|
||||
revoked.push(url);
|
||||
});
|
||||
vi.stubGlobal('Image', FakeImage);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
URL.createObjectURL = realCreate;
|
||||
URL.revokeObjectURL = realRevoke;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
/** Frisches Modul je Test, damit der Bild-Cache leer startet. */
|
||||
async function fresh() {
|
||||
vi.resetModules();
|
||||
return import('./marks');
|
||||
}
|
||||
|
||||
it('gleiches (id, tone) liefert dieselbe Promise und lädt nur einmal, anderer Ton eine andere', async () => {
|
||||
const m = await fresh();
|
||||
const visa = m.getNetworkMark('visa-auto');
|
||||
const a = m.loadMarkImage(visa, 'dark');
|
||||
const b = m.loadMarkImage(visa, 'dark');
|
||||
const c = m.loadMarkImage(visa, 'light');
|
||||
const d = m.loadMarkImage(m.getNetworkMark('jcb'), 'dark');
|
||||
expect(b).toBe(a);
|
||||
expect(c).not.toBe(a);
|
||||
expect(d).not.toBe(a);
|
||||
const imgs = await Promise.all([a, c, d]);
|
||||
expect(imgs[0]).toBeInstanceOf(FakeImage);
|
||||
expect(blobs).toHaveLength(3);
|
||||
// nach dem Laden bleibt der Eintrag im Cache
|
||||
expect(m.loadMarkImage(visa, 'dark')).toBe(a);
|
||||
expect(blobs).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('Blob ist ein eingefärbtes SVG mit eigener Größe und dem richtigen MIME-Typ', async () => {
|
||||
const m = await fresh();
|
||||
const visa = m.getNetworkMark('visa-auto');
|
||||
await m.loadMarkImage(visa, 'dark');
|
||||
await m.loadMarkImage(visa, 'light');
|
||||
const [dark, light] = await Promise.all(blobs.map((b) => b.text()));
|
||||
expect(blobs[0].type).toBe('image/svg+xml');
|
||||
expect(dark).toContain('#111111');
|
||||
expect(dark).not.toContain('currentColor');
|
||||
expect(light).toContain('#FFFFFF');
|
||||
expect(light).not.toContain('#111111');
|
||||
const root = /^<svg\b[^>]*>/.exec(dark)![0];
|
||||
const w = Number(/\swidth="([\d.]+)"/.exec(root)![1]);
|
||||
const h = Number(/\sheight="([\d.]+)"/.exec(root)![1]);
|
||||
expect(h).toBe(visa.maxH * 4);
|
||||
expect(w / h).toBeCloseTo(m.svgAspect(visa.svg), 1);
|
||||
// Bild bekommt die Blob-URL
|
||||
expect(env.images[0].src).toBe('blob:fake/1');
|
||||
expect(env.images[1].src).toBe('blob:fake/2');
|
||||
});
|
||||
|
||||
it('Marken ohne currentColor bleiben im Blob unverändert (bis auf die Größe)', async () => {
|
||||
const m = await fresh();
|
||||
const jcb = m.getNetworkMark('jcb');
|
||||
await m.loadMarkImage(jcb, 'dark');
|
||||
const text = await blobs[0].text();
|
||||
expect(text.replace(/^<svg width="[\d.]+" height="[\d.]+"/, '<svg')).toBe(jcb.svg);
|
||||
});
|
||||
|
||||
it('gibt die Blob-URL nach dem Laden frei', async () => {
|
||||
const m = await fresh();
|
||||
await m.loadMarkImage(m.getNetworkMark('visa-blue'), 'light');
|
||||
expect(revoked).toEqual(['blob:fake/1']);
|
||||
});
|
||||
|
||||
it('Fehler: Promise wird abgelehnt, URL freigegeben, Cache-Eintrag verworfen, zweiter Aufruf versucht es neu', async () => {
|
||||
const m = await fresh();
|
||||
const def = m.getNetworkMark('visa-auto');
|
||||
env.mode = 'error';
|
||||
const first = m.loadMarkImage(def, 'dark');
|
||||
await expect(first).rejects.toThrow(/visa-auto/);
|
||||
expect(revoked).toEqual(['blob:fake/1']);
|
||||
|
||||
env.mode = 'load';
|
||||
const second = m.loadMarkImage(def, 'dark');
|
||||
expect(second).not.toBe(first);
|
||||
await expect(second).resolves.toBeInstanceOf(FakeImage);
|
||||
expect(blobs).toHaveLength(2);
|
||||
expect(revoked).toEqual(['blob:fake/1', 'blob:fake/2']);
|
||||
// der erfolgreiche Versuch bleibt zwischengespeichert
|
||||
expect(m.loadMarkImage(def, 'dark')).toBe(second);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user