fix(logos): unerwartete Fehler beim Vorbereiten eines Logos in die Konsole
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
df1cf29d42
commit
4232afdf35
2 files changed
+78
-4
No files matched your search
@@ -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 { setLang } from '../i18n/i18n';
|
||||||
import {
|
import {
|
||||||
ALPHA_EMPTY,
|
ALPHA_EMPTY,
|
||||||
@@ -458,6 +458,8 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
/** Die Maße, die ein geladenes Bild meldet (naturalWidth/naturalHeight). */
|
/** Die Maße, die ein geladenes Bild meldet (naturalWidth/naturalHeight). */
|
||||||
let natural: { w: number; h: number };
|
let natural: { w: number; h: number };
|
||||||
let decodeCalls: 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<typeof console.warn>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ersatz für `Image`, wie `decodeImage` (editor/upload.ts) es braucht: meldet nach dem Setzen von `src` „geladen“ mit den Maßen
|
* 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).
|
* das Ergebnis (überall deckend, mit `resultHole` fehlt dem ersten Pixel die Deckkraft).
|
||||||
*/
|
*/
|
||||||
function stubCanvas(
|
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 {
|
): void {
|
||||||
const create = document.createElement.bind(document);
|
const create = document.createElement.bind(document);
|
||||||
vi.spyOn(document, 'createElement').mockImplementation(((tag: string) => {
|
vi.spyOn(document, 'createElement').mockImplementation(((tag: string) => {
|
||||||
@@ -505,7 +515,10 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
set imageSmoothingQuality(value: string) {
|
set imageSmoothingQuality(value: string) {
|
||||||
canvas.smoothing = value;
|
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) => ({
|
getImageData: (_x: number, _y: number, w: number, h: number) => ({
|
||||||
data: isSource
|
data: isSource
|
||||||
? opts.blank
|
? opts.blank
|
||||||
@@ -533,6 +546,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
imageFails = false;
|
imageFails = false;
|
||||||
natural = { w: 100, h: 40 };
|
natural = { w: 100, h: 40 };
|
||||||
decodeCalls = 0;
|
decodeCalls = 0;
|
||||||
|
warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
vi.spyOn(URL, 'createObjectURL').mockImplementation((blob) => {
|
vi.spyOn(URL, 'createObjectURL').mockImplementation((blob) => {
|
||||||
blobs.push(blob as Blob);
|
blobs.push(blob as Blob);
|
||||||
return `blob:test-${blobs.length}`;
|
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('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"/>'))).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<typeof stubCanvas>[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', () => {
|
describe('vorab geprüft', () => {
|
||||||
it('ein falscher Typ wird abgelehnt, ohne etwas zu laden', async () => {
|
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);
|
await expect(prepareLogo(new File(['x'], 'a.gif', { type: 'image/gif' }))).rejects.toThrow(TYPE);
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ export async function prepareLogo(file: File): Promise<PreparedLogo> {
|
|||||||
hasTransparency: hasTransparency(out.ctx.getImageData(0, 0, size.w, size.h).data),
|
hasTransparency: hasTransparency(out.ctx.getImageData(0, 0, size.w, size.h).data),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user