fix(card): fehlendes Logo bricht nur ab, wenn die Vorlage es zeichnet; Download aus „Meine Karten“ meldet es
Entscheidung des Controllers zu Task 5: Ein fehlendes Community-Logo ist beim Rendern einer gespeicherten Karte nur dann ein Problem, wenn die Vorlage es zeichnet (layoutFor(template).logo). Bei „Nur Bild“ wird ein Logo nie gezeichnet, dort fehlt nichts. Der Editor-Hinweis (Auswahlzustand) und der Download im Editor bleiben unverändert. - community/publish.ts: neue gemeinsame Prüfung assertLogoAvailable(config, assets); sie wirft LOGO_MISSING_PUBLISH, wenn assets.logoMissing gilt und die Vorlage das Logo zeichnet (minimal, classic, signature). renderForGallery nutzt sie: bei einer zeichnenden Vorlage Abbruch vor dem Rendern (nichts hochgeladen), bei „Nur Bild“ wird normal gerendert und veröffentlicht. - pages/cards-page.ts: der Download einer gespeicherten Karte ruft dieselbe Prüfung. Statt stillschweigend ohne Logo herunterzuladen, zeigt die Kachel den Text LOGO_MISSING_PUBLISH über den vorhandenen Fehlermechanismus (run() und failureText() schreiben in .card-msg), übersetzt mit t(); es wird nichts exportiert oder gespeichert. Bei „Nur Bild“ läuft der Download normal. - Tests: publish.test.ts (classic/minimal/signature mit fehlendem Logo brechen ab, „Nur Bild“ veröffentlicht normal, Wahrheitstabelle für assertLogoAvailable) und cards-page.test.ts (Download mit fehlendem Logo: Meldung, kein Download; „Nur Bild“ und vorhandenes Logo: Download wie sonst; zweiter Versuch; Englisch). Vitest 45 Dateien, 1338 Tests grün, tsc und Build sauber. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0be5abd954
commit
8205f7c37f
4 files changed
+147
-14
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DEFAULT_CONFIG } from '../card/config';
|
||||
import { DEFAULT_CONFIG, type TemplateId } from '../card/config';
|
||||
import type { SavedCard } from '../account/cards';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -30,6 +30,7 @@ import { ApiError, publishCard } from './api';
|
||||
import {
|
||||
LOGO_MISSING_PUBLISH,
|
||||
PNG_MAX_BYTES,
|
||||
assertLogoAvailable,
|
||||
normalizeTags,
|
||||
publishSavedCard,
|
||||
renderForGallery,
|
||||
@@ -263,7 +264,12 @@ describe('publishSavedCard', () => {
|
||||
describe('Logo der Karte beim Veröffentlichen', () => {
|
||||
const photo = { source: {}, w: 3000, h: 2000 };
|
||||
const source = new Blob(['src'], { type: 'image/jpeg' });
|
||||
const withLogo = (logo: string | null): SavedCard => ({ ...card, config: { ...DEFAULT_CONFIG, logo } });
|
||||
const withLogo = (logo: string | null, template: TemplateId = DEFAULT_CONFIG.template): SavedCard => ({
|
||||
...card,
|
||||
config: { ...DEFAULT_CONFIG, template, logo },
|
||||
});
|
||||
/** Die Vorlagen, die das Community-Logo zeichnen (alle außer „Nur Bild“). */
|
||||
const DRAWING = ['minimal', 'classic', 'signature'] as const;
|
||||
const assetsFor = (patch: Record<string, unknown> = {}) => ({ image: photo, logo: null, brand: null, logoMissing: false, ...patch });
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -304,10 +310,21 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
||||
expect(out.png.type).toBe('image/png');
|
||||
});
|
||||
|
||||
it('fehlendes Logo (gelöscht oder ausgeblendet): Abbruch mit LOGO_MISSING_PUBLISH, es wird nichts gerendert', async () => {
|
||||
it.each(DRAWING)(
|
||||
'fehlendes Logo (gelöscht oder ausgeblendet), Vorlage %s zeichnet es: Abbruch mit LOGO_MISSING_PUBLISH, es wird nichts gerendert',
|
||||
async (template) => {
|
||||
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||
await expect(renderForGallery(withLogo('gone', template), source)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('fehlendes Logo bei „Nur Bild“ (image): die Vorlage zeichnet es nie, es wird ganz normal gerendert', async () => {
|
||||
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||
await expect(renderForGallery(withLogo('gone'), source)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||
const out = await renderForGallery(withLogo('gone', 'image'), source);
|
||||
expect(mocks.exportCard).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.exportCard.mock.calls[0][2]).toEqual({ width: 1536, type: 'image/png' });
|
||||
expect([out.png.type, out.preview.type]).toEqual(['image/png', 'image/webp']);
|
||||
});
|
||||
|
||||
it('ein Ladefehler der Logos (Netz) ist kein fehlendes Logo: der Fehler kommt unverändert an', async () => {
|
||||
@@ -316,16 +333,24 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
||||
await expect(renderForGallery(withLogo('abc123'), source)).rejects.toBe(failure);
|
||||
});
|
||||
|
||||
it('publishSavedCard: mit fehlendem Logo wird nichts hochgeladen, nichts gelöscht und die API nicht gerufen', async () => {
|
||||
it('publishSavedCard: classic mit fehlendem Logo – nichts wird hochgeladen, nichts gelöscht, die API nicht gerufen', async () => {
|
||||
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||
const fake = fakeDeps({ render: renderForGallery });
|
||||
await expect(publishSavedCard(withLogo('gone'), form, fake.deps)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||
await expect(publishSavedCard(withLogo('gone', 'classic'), form, fake.deps)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||
expect(fake.names()).toEqual(['userId', 'loadImage']);
|
||||
expect(fake.of('createFile')).toHaveLength(0);
|
||||
expect(fake.of('deleteFile')).toHaveLength(0);
|
||||
expect(fake.of('publishCard')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('publishSavedCard: „Nur Bild“ (image) mit fehlendem Logo – wird veröffentlicht, als wäre nichts', async () => {
|
||||
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||
const fake = fakeDeps({ render: renderForGallery });
|
||||
await expect(publishSavedCard(withLogo('gone', 'image'), form, fake.deps)).resolves.toEqual({ id: 'g1' });
|
||||
expect(fake.names()).toEqual(['userId', 'loadImage', 'createFile', 'createFile', 'publishCard']);
|
||||
expect(fake.of('deleteFile')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('publishSavedCard: mit vorhandenem Logo läuft alles wie sonst', async () => {
|
||||
const fake = fakeDeps({ render: renderForGallery });
|
||||
await expect(publishSavedCard(withLogo('abc123'), form, fake.deps)).resolves.toEqual({ id: 'g1' });
|
||||
@@ -333,6 +358,23 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertLogoAvailable', () => {
|
||||
const config = (template: TemplateId) => ({ ...DEFAULT_CONFIG, template, logo: 'abc123' });
|
||||
const assets = (logoMissing: boolean) => ({ image: null, logo: null, brand: null, logoMissing });
|
||||
|
||||
it.each(['minimal', 'classic', 'signature'] as const)('Logo fehlt und Vorlage %s zeichnet es: wirft LOGO_MISSING_PUBLISH', (template) => {
|
||||
expect(() => assertLogoAvailable(config(template), assets(true))).toThrow(LOGO_MISSING_PUBLISH);
|
||||
});
|
||||
|
||||
it('Logo fehlt, aber „Nur Bild“ zeichnet nie ein Logo: wirft nicht', () => {
|
||||
expect(() => assertLogoAvailable(config('image'), assets(true))).not.toThrow();
|
||||
});
|
||||
|
||||
it.each(['image', 'minimal', 'classic', 'signature'] as const)('Logo ist da (oder keins gewählt): Vorlage %s wirft nicht', (template) => {
|
||||
expect(() => assertLogoAvailable(config(template), assets(false))).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mit der echten API-Schicht (publishCard, apiPost) und gestubbtem fetch', () => {
|
||||
const json = (status: number, body: unknown) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
||||
|
||||
Reference in new issue
Block a user