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
@@ -1 +1 @@
|
|||||||
<svg viewBox="0 0 1000.01 997.53" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-width="36" d="M18 18h961.517v961.517H18z"/><path fill="currentColor" d="M50.628 352.584.193 469.848h32.836l9.306-23.482h54.1l9.257 23.482h33.56L88.863 352.584zm18.66 27.29 16.49 41.034H52.75zm73.435 89.954V352.564l46.661.173 27.14 75.605 26.49-75.778h46.289v117.264h-29.316v-86.405l-31.076 86.405h-25.71l-31.162-86.405v86.405zm166.638 0V352.564h95.663v26.23h-66.038v20.059h64.495v24.687h-64.495v20.83h66.038v25.458zm112.636-117.244v117.264h29.316v-41.66h12.343l35.15 41.66h35.826l-38.574-43.202c15.831-1.336 32.161-14.924 32.161-36.019 0-24.676-19.368-38.043-40.984-38.043zm29.316 26.23h33.51c8.04 0 13.887 6.288 13.887 12.344 0 7.79-7.577 12.343-13.452 12.343h-33.945zm118.807 91.014h-29.933V352.564h29.933zm70.975 0h-6.46c-31.262 0-50.243-24.629-50.243-58.15 0-34.348 18.768-59.114 58.246-59.114h32.402v27.773h-33.586c-16.026 0-27.36 12.507-27.36 31.63 0 22.71 12.96 32.248 31.63 32.248h7.715zm63.792-117.244-50.435 117.264h32.836l9.305-23.482h54.1l9.258 23.482h33.559l-50.387-117.264zm18.66 27.29 16.49 41.034h-33.029zm73.386 89.954V352.564h37.272l47.59 73.676v-73.676h29.317v117.264h-36.067l-48.796-75.604v75.604zM213.699 675.04V557.777h95.662v26.23h-66.038v20.059h64.495v24.687h-64.495v20.83h66.038v25.459zm468.748 0V557.777h95.662v26.23h-66.038v20.059h64.187v24.687H712.07v20.83h66.038v25.459zm-369.373 0 46.578-57.908-47.687-59.356H348.9l28.4 36.694 28.497-36.694h35.488l-47.06 58.633 46.663 58.632H403.96l-27.576-36.115-26.905 36.115zM444.37 557.796V675.06h30.087v-37.03h30.859c26.111 0 45.903-13.853 45.903-40.792 0-22.317-15.523-39.442-42.094-39.442zm30.087 26.52h32.498c8.436 0 14.465 5.17 14.465 13.5 0 7.827-5.999 13.501-14.561 13.501h-32.402zm89.491-26.54v117.265h29.316v-41.66h12.344l35.15 41.66h35.825l-38.573-43.203c15.83-1.336 32.16-14.923 32.16-36.018 0-24.677-19.368-38.044-40.984-38.044zm29.316 26.23h33.511c8.039 0 13.887 6.289 13.887 12.344 0 7.79-7.577 12.344-13.453 12.344h-33.945zm198.423 91.034v-25.458h58.671c8.681 0 12.44-4.691 12.44-9.836 0-4.93-3.747-9.914-12.44-9.914h-26.513c-23.045 0-35.88-14.04-35.88-35.121 0-18.803 11.753-36.935 46-36.935h57.088l-12.343 26.385h-49.375c-9.438 0-12.343 4.952-12.343 9.682 0 4.861 3.59 10.222 10.8 10.222h27.773c25.69 0 36.838 14.572 36.838 33.655 0 20.517-12.422 37.32-38.236 37.32zm107.597 0v-25.458h58.67c8.682 0 12.44-4.691 12.44-9.836 0-4.93-3.746-9.914-12.44-9.914h-26.512c-23.046 0-35.88-14.04-35.88-35.121 0-18.803 11.753-36.935 45.999-36.935h57.089l-12.344 26.385h-49.374c-9.438 0-12.344 4.952-12.344 9.682 0 4.861 3.59 10.222 10.801 10.222h27.773c25.69 0 36.838 14.572 36.838 33.655 0 20.517-12.422 37.32-38.236 37.32z"/></svg>
|
<svg viewBox="-66 -67.24 1132.01 1132.01" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-width="36" d="M-48-49.24h1096v1096H-48z"/><path fill="currentColor" d="M50.628 352.584.193 469.848h32.836l9.306-23.482h54.1l9.257 23.482h33.56L88.863 352.584zm18.66 27.29 16.49 41.034H52.75zm73.435 89.954V352.564l46.661.173 27.14 75.605 26.49-75.778h46.289v117.264h-29.316v-86.405l-31.076 86.405h-25.71l-31.162-86.405v86.405zm166.638 0V352.564h95.663v26.23h-66.038v20.059h64.495v24.687h-64.495v20.83h66.038v25.458zm112.636-117.244v117.264h29.316v-41.66h12.343l35.15 41.66h35.826l-38.574-43.202c15.831-1.336 32.161-14.924 32.161-36.019 0-24.676-19.368-38.043-40.984-38.043zm29.316 26.23h33.51c8.04 0 13.887 6.288 13.887 12.344 0 7.79-7.577 12.343-13.452 12.343h-33.945zm118.807 91.014h-29.933V352.564h29.933zm70.975 0h-6.46c-31.262 0-50.243-24.629-50.243-58.15 0-34.348 18.768-59.114 58.246-59.114h32.402v27.773h-33.586c-16.026 0-27.36 12.507-27.36 31.63 0 22.71 12.96 32.248 31.63 32.248h7.715zm63.792-117.244-50.435 117.264h32.836l9.305-23.482h54.1l9.258 23.482h33.559l-50.387-117.264zm18.66 27.29 16.49 41.034h-33.029zm73.386 89.954V352.564h37.272l47.59 73.676v-73.676h29.317v117.264h-36.067l-48.796-75.604v75.604zM213.699 675.04V557.777h95.662v26.23h-66.038v20.059h64.495v24.687h-64.495v20.83h66.038v25.459zm468.748 0V557.777h95.662v26.23h-66.038v20.059h64.187v24.687H712.07v20.83h66.038v25.459zm-369.373 0 46.578-57.908-47.687-59.356H348.9l28.4 36.694 28.497-36.694h35.488l-47.06 58.633 46.663 58.632H403.96l-27.576-36.115-26.905 36.115zM444.37 557.796V675.06h30.087v-37.03h30.859c26.111 0 45.903-13.853 45.903-40.792 0-22.317-15.523-39.442-42.094-39.442zm30.087 26.52h32.498c8.436 0 14.465 5.17 14.465 13.5 0 7.827-5.999 13.501-14.561 13.501h-32.402zm89.491-26.54v117.265h29.316v-41.66h12.344l35.15 41.66h35.825l-38.573-43.203c15.83-1.336 32.16-14.923 32.16-36.018 0-24.677-19.368-38.044-40.984-38.044zm29.316 26.23h33.511c8.039 0 13.887 6.289 13.887 12.344 0 7.79-7.577 12.344-13.453 12.344h-33.945zm198.423 91.034v-25.458h58.671c8.681 0 12.44-4.691 12.44-9.836 0-4.93-3.747-9.914-12.44-9.914h-26.513c-23.045 0-35.88-14.04-35.88-35.121 0-18.803 11.753-36.935 46-36.935h57.088l-12.343 26.385h-49.375c-9.438 0-12.343 4.952-12.343 9.682 0 4.861 3.59 10.222 10.8 10.222h27.773c25.69 0 36.838 14.572 36.838 33.655 0 20.517-12.422 37.32-38.236 37.32zm107.597 0v-25.458h58.67c8.682 0 12.44-4.691 12.44-9.836 0-4.93-3.746-9.914-12.44-9.914h-26.512c-23.046 0-35.88-14.04-35.88-35.121 0-18.803 11.753-36.935 45.999-36.935h57.089l-12.344 26.385h-49.374c-9.438 0-12.344 4.952-12.344 9.682 0 4.861 3.59 10.222 10.801 10.222h27.773c25.69 0 36.838 14.572 36.838 33.655 0 20.517-12.422 37.32-38.236 37.32z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
+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 { NETWORK_IDS } from './config';
|
||||||
import favicon from '../../public/favicon.svg?raw';
|
import favicon from '../../public/favicon.svg?raw';
|
||||||
import {
|
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', () => {
|
describe('maxH', () => {
|
||||||
const h = (id: string) => NETWORK_MARKS.find((m) => m.id === id)!.maxH;
|
const h = (id: string) => NETWORK_MARKS.find((m) => m.id === id)!.maxH;
|
||||||
|
|
||||||
@@ -159,7 +173,124 @@ describe('Favicon', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('loadMarkImage', () => {
|
describe('loadMarkImage', () => {
|
||||||
it('ist eine Funktion', () => {
|
type Mode = 'load' | 'error';
|
||||||
expect(typeof loadMarkImage).toBe('function');
|
|
||||||
|
/** 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