diff --git a/web/src/logos/prepare-logo.test.ts b/web/src/logos/prepare-logo.test.ts index 9afd898..adb15a7 100644 --- a/web/src/logos/prepare-logo.test.ts +++ b/web/src/logos/prepare-logo.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'; import { setLang } from '../i18n/i18n'; import { ALPHA_EMPTY, @@ -458,6 +458,8 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => { /** Die Maße, die ein geladenes Bild meldet (naturalWidth/naturalHeight). */ let natural: { w: number; h: number }; let decodeCalls: number; + /** Unerwartete Fehler gehen als Warnung in die Konsole (siehe prepareLogo); sie bleibt hier stumm und lässt sich prüfen. */ + let warn: MockInstance; /** * Ersatz für `Image`, wie `decodeImage` (editor/upload.ts) es braucht: meldet nach dem Setzen von `src` „geladen“ mit den Maßen @@ -494,7 +496,15 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => { * das Ergebnis (überall deckend, mit `resultHole` fehlt dem ersten Pixel die Deckkraft). */ function stubCanvas( - opts: { visible?: (x: number, y: number) => number; blank?: boolean; resultHole?: boolean; noBlob?: boolean; noContext?: boolean } = {}, + opts: { + visible?: (x: number, y: number) => number; + blank?: boolean; + resultHole?: boolean; + noBlob?: boolean; + noContext?: boolean; + /** `drawImage` wirft diesen Fehler. */ + drawError?: unknown; + } = {}, ): void { const create = document.createElement.bind(document); vi.spyOn(document, 'createElement').mockImplementation(((tag: string) => { @@ -505,7 +515,10 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => { set imageSmoothingQuality(value: string) { canvas.smoothing = value; }, - drawImage: (...args: unknown[]) => void canvas.draws.push(args), + drawImage: (...args: unknown[]) => { + if (opts.drawError) throw opts.drawError; + canvas.draws.push(args); + }, getImageData: (_x: number, _y: number, w: number, h: number) => ({ data: isSource ? opts.blank @@ -533,6 +546,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => { imageFails = false; natural = { w: 100, h: 40 }; decodeCalls = 0; + warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.spyOn(URL, 'createObjectURL').mockImplementation((blob) => { blobs.push(blob as Blob); return `blob:test-${blobs.length}`; @@ -725,6 +739,62 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => { }); }); + describe('unerwartete Fehler: eine Warnung mit dem Originalfehler in der Konsole, der Nutzer liest weiter „nicht gelesen“', () => { + it('der Browser wirft beim Zeichnen: genau eine Warnung mit genau diesem Fehler, die Meldung bleibt unverändert', async () => { + const boom = new DOMException('The source image could not be decoded.', 'InvalidStateError'); + stubCanvas({ drawError: boom }); + await expect(prepareLogo(rasterFile())).rejects.toThrow(READ); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith(expect.any(String), boom); + }); + + it('auch bei einem SVG', async () => { + const boom = new TypeError('boom'); + stubCanvas({ drawError: boom }); + await expect(prepareLogo(svgFile(''))).rejects.toThrow(READ); + expect(warn).toHaveBeenCalledExactlyOnceWith(expect.any(String), boom); + }); + + it('das Bild lässt sich nicht laden: eine Warnung mit dem Fehler des Dekodierens', async () => { + imageFails = true; + stubCanvas(); + await expect(prepareLogo(rasterFile())).rejects.toThrow(READ); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][1]).toBeInstanceOf(Error); + }); + + it('der Text für den Nutzer ist in jeder Sprache der gewohnte, die Warnung ändert ihn nicht', async () => { + setLang('en', { persist: false }); + stubCanvas({ drawError: new Error('boom') }); + await expect(prepareLogo(rasterFile())).rejects.toThrow('The logo couldn’t be read.'); + expect(warn).toHaveBeenCalledTimes(1); + }); + + it('Fehler mit eigener Meldung (leer, zu klein, zu groß, kein Canvas, kein PNG, falscher Typ) sind erwartet: keine Warnung', async () => { + /** Ein neuer Versuch mit frisch gestellten Canvas (der erste Canvas eines Versuchs trägt das Quellbild). */ + const attempt = (opts: Parameters[0], file: File = rasterFile()) => { + canvases = []; + stubCanvas(opts); + return prepareLogo(file); + }; + await expect(attempt({ visible: () => 0 })).rejects.toThrow(EMPTY); + await expect(attempt({ visible: only(10, 5, 7, 7) })).rejects.toThrow(SMALL); + natural = { w: 8001, h: 5000 }; + await expect(attempt({})).rejects.toThrow(PIXELS); + natural = { w: 100, h: 40 }; + await expect(attempt({ noContext: true })).rejects.toThrow(READ); + await expect(attempt({ visible: only(10, 5, 20, 10), noBlob: true })).rejects.toThrow(READ); + await expect(attempt({}, new File(['x'], 'a.gif', { type: 'image/gif' }))).rejects.toThrow(TYPE); + expect(warn).not.toHaveBeenCalled(); + }); + + it('ein gelungenes Vorbereiten warnt nicht', async () => { + stubCanvas({ visible: only(10, 5, 20, 10) }); + await prepareLogo(rasterFile()); + expect(warn).not.toHaveBeenCalled(); + }); + }); + describe('vorab geprüft', () => { it('ein falscher Typ wird abgelehnt, ohne etwas zu laden', async () => { await expect(prepareLogo(new File(['x'], 'a.gif', { type: 'image/gif' }))).rejects.toThrow(TYPE); diff --git a/web/src/logos/prepare-logo.ts b/web/src/logos/prepare-logo.ts index 995045f..7f245ba 100644 --- a/web/src/logos/prepare-logo.ts +++ b/web/src/logos/prepare-logo.ts @@ -268,6 +268,10 @@ export async function prepareLogo(file: File): Promise { hasTransparency: hasTransparency(out.ctx.getImageData(0, 0, size.w, size.h).data), }; } catch (err) { - throw err instanceof LogoError ? err : new LogoError(t(LOGO_READ_MESSAGE)); + if (err instanceof LogoError) throw err; + // Alles andere (Bild nicht ladbar, Fehler des Browsers beim Zeichnen …) heißt für den Nutzer „nicht lesbar“; für die Fehlersuche bleibt + // der eigentliche Anlass in der Konsole. + console.warn('Logo konnte nicht vorbereitet werden', err); + throw new LogoError(t(LOGO_READ_MESSAGE)); } }