From 4232afdf354110d6380bc1712a0c0f82c6a2d040 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 01:20:28 +0000 Subject: [PATCH] fix(logos): unerwartete Fehler beim Vorbereiten eines Logos in die Konsole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schlussreview F6. prepareLogo machte aus jedem Fehler außer seinen eigenen („leer“, „zu klein“ …) die Meldung „Das Logo konnte nicht gelesen werden.“ und verlor dabei den Anlass (Fehler des Browsers beim Zeichnen, Dekodierfehler): bei einer Rückmeldung eines Nutzers war nichts mehr nachzuvollziehen. - Vor dem Umwandeln steht jetzt genau eine console.warn mit dem Originalfehler. Der Text für den Nutzer bleibt unverändert (in beiden Sprachen). Fehler mit eigener Meldung (LogoError: Typ, Größe, zu viele Pixel, leer, zu klein, kein Canvas, kein PNG) sind erwartet und warnen nicht. - Tests: Browserfehler beim Zeichnen (Raster und SVG), Dekodierfehler, Englisch, keine Warnung bei den erwarteten Fehlern und bei Erfolg. Die Tests der Gruppe „Bild und Canvas gestellt“ ersetzen console.warn durch eine Attrappe, damit die Ausgabe sauber bleibt. Co-Authored-By: Claude Opus 5.5 --- web/src/logos/prepare-logo.test.ts | 76 ++++++++++++++++++++++++++++-- web/src/logos/prepare-logo.ts | 6 ++- 2 files changed, 78 insertions(+), 4 deletions(-) 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)); } }