fix(logos): Logo vor Veröffentlichen und Herunterladen frisch prüfen
Schlussreview F4. Zeilen und Bilder der Community-Logos (logo-asset.ts) bleiben bis zum Neuladen der Seite im Speicher. Wurde ein Logo danach ausgeblendet oder gelöscht, konnte es aus diesem Zwischenspeicher in ein veröffentlichtes Kartenbild oder in ein heruntergeladenes PNG gelangen. - renderForGallery (Veröffentlichen), der Download im Editor und der Download in „Meine Karten“ rufen vor dem Laden der Bilder forgetLogo(config.logo) (falls eines gewählt ist): Zeile und Bild werden neu vom Server geholt, hidden/fehlt wird also neu geprüft. Veröffentlichen bricht bei einem fehlenden Logo wie bisher mit LOGO_MISSING_PUBLISH ab, „Meine Karten“ meldet es an der Kachel. - Der Download im Editor berücksichtigt assets.logoMissing wie der Hinweis im Editor: das PNG entsteht ohne das fehlende Logo (die Vorschau zeigte es bis dahin noch), der Hinweis „Das gewählte Logo ist nicht mehr verfügbar.“ erscheint und die Vorschau zeichnet ohne Logo, damit PNG und Vorschau dasselbe zeigen. Ist ein Logo wieder da, folgt die Vorschau ebenso. Das gilt nur, solange die Auswahl noch dieselbe ist. - Tests mit dem echten Zwischenspeicher (nur Zeile und Datei aus logos.ts ersetzt): erst geladen, dann ausgeblendet bzw. gelöscht → neu geprüft, Veröffentlichen bricht ab / „Meine Karten“ meldet / der Editor lädt ohne Logo herunter, zeigt den Hinweis (auch zusammen mit dem Größenhinweis) und zeichnet neu; sichtbares Logo wird frisch geholt; Karte ohne Logo holt nichts; „Nur Bild“ bleibt unberührt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3c6e65801
commit
4aedb1d322
6 files changed
+323
-2
No files matched your search
@@ -7,6 +7,8 @@ const mocks = vi.hoisted(() => ({
|
||||
decodeImage: vi.fn(),
|
||||
loadCardAssets: vi.fn(),
|
||||
exportCard: vi.fn(),
|
||||
getLogo: vi.fn(),
|
||||
loadLogoBlob: vi.fn(),
|
||||
}));
|
||||
|
||||
// Die Standard-Abhängigkeiten (Appwrite, Canvas) kommen in diesen Tests nie zum Einsatz;
|
||||
@@ -21,11 +23,15 @@ vi.mock('../account/appwrite', () => ({
|
||||
}));
|
||||
// Rendern (Bild dekodieren, Logos laden, Canvas) ist für renderForGallery ersetzt.
|
||||
vi.mock('../card/render', () => ({ loadCardAssets: mocks.loadCardAssets, exportCard: mocks.exportCard }));
|
||||
// Zeile und Datei eines Community-Logos kommen aus der Datenschicht; nur der Zwischenspeicher darüber (logo-asset.ts) ist echt.
|
||||
vi.mock('../logos/logos', () => ({ getLogo: mocks.getLogo, loadLogoBlob: mocks.loadLogoBlob }));
|
||||
vi.mock('../editor/upload', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../editor/upload')>()),
|
||||
decodeImage: mocks.decodeImage,
|
||||
}));
|
||||
|
||||
import { forgetLogo, loadLogoAsset } from '../logos/logo-asset';
|
||||
import type { CommunityLogo } from '../logos/logos';
|
||||
import { ApiError, publishCard } from './api';
|
||||
import {
|
||||
LOGO_MISSING_PUBLISH,
|
||||
@@ -358,6 +364,96 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zwischenspeicher der Logos beim Veröffentlichen (ein ausgeblendetes Logo darf nicht in die Galerie)', () => {
|
||||
const photo = { source: {}, w: 3000, h: 2000 };
|
||||
const source = new Blob(['src'], { type: 'image/jpeg' });
|
||||
const logoRow = (patch: Partial<CommunityLogo> = {}): CommunityLogo => ({
|
||||
id: 'zw1',
|
||||
name: 'Mein Logo',
|
||||
displayName: 'Kenso',
|
||||
fileId: 'zw1',
|
||||
ownerId: 'u1',
|
||||
mono: false,
|
||||
likeCount: 0,
|
||||
hidden: false,
|
||||
reportCount: 0,
|
||||
createdAt: '2026-10-04T10:00:00.000Z',
|
||||
...patch,
|
||||
});
|
||||
const withLogo = (id: string, template: TemplateId = 'classic'): SavedCard => ({ ...card, config: { ...DEFAULT_CONFIG, template, logo: id } });
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.decodeImage.mockReset().mockResolvedValue(photo);
|
||||
mocks.exportCard
|
||||
.mockReset()
|
||||
.mockImplementation(async (_config: unknown, _assets: unknown, opts: { type: string }) => new Blob(['x'], { type: opts.type }));
|
||||
mocks.getLogo.mockReset().mockResolvedValue(logoRow());
|
||||
mocks.loadLogoBlob.mockReset().mockResolvedValue(new Blob(['png'], { type: 'image/png' }));
|
||||
// Wie der echte Lader (card/render.ts: loadCardAssets), mit dem echten Zwischenspeicher der Logos darunter.
|
||||
mocks.loadCardAssets.mockReset().mockImplementation(async (config: { logo: string | null; details: 'light' | 'dark' }, image: unknown) => {
|
||||
const logo = config.logo ? await loadLogoAsset(config.logo, config.details) : null;
|
||||
return { image, logo, brand: null, logoMissing: Boolean(config.logo) && logo === null };
|
||||
});
|
||||
vi.stubGlobal('createImageBitmap', async () => ({ width: 200, height: 100 }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
forgetLogo('zw1');
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('ein Logo, das nach dem ersten Laden (Editor, Zwischenspeicher) ausgeblendet wurde, wird neu geprüft: Abbruch, nichts wird hochgeladen', async () => {
|
||||
await expect(loadLogoAsset('zw1', 'light')).resolves.not.toBeNull(); // der Editor hat es geladen: Zeile und Bild stecken im Zwischenspeicher
|
||||
mocks.getLogo.mockResolvedValue(logoRow({ hidden: true })); // dann hat ein Admin oder die Melder es ausgeblendet
|
||||
const fake = fakeDeps({ render: renderForGallery });
|
||||
await expect(publishSavedCard(withLogo('zw1'), form, fake.deps)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||
expect(mocks.getLogo).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||
expect(fake.of('createFile')).toHaveLength(0);
|
||||
expect(fake.of('publishCard')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ein Logo, das nach dem ersten Laden gelöscht wurde, ebenso', async () => {
|
||||
await expect(loadLogoAsset('zw1', 'light')).resolves.not.toBeNull();
|
||||
mocks.getLogo.mockResolvedValue(null);
|
||||
await expect(renderForGallery(withLogo('zw1'), source)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ein weiterhin sichtbares Logo wird mit frisch geholter Zeile und Datei gerendert (kein Zwischenspeicher-Treffer)', async () => {
|
||||
await expect(loadLogoAsset('zw1', 'light')).resolves.not.toBeNull();
|
||||
const out = await renderForGallery(withLogo('zw1'), source);
|
||||
expect(out.png.type).toBe('image/png');
|
||||
expect(mocks.getLogo).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.loadLogoBlob).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.exportCard.mock.calls[0][1]).toMatchObject({ logo: { w: 200, h: 100 } });
|
||||
});
|
||||
|
||||
it('beide Tonarten des Logos werden vergessen, nicht nur die der Karte', async () => {
|
||||
await loadLogoAsset('zw1', 'light');
|
||||
await loadLogoAsset('zw1', 'dark');
|
||||
mocks.getLogo.mockResolvedValue(logoRow({ hidden: true }));
|
||||
await expect(renderForGallery({ ...withLogo('zw1'), config: { ...withLogo('zw1').config, details: 'dark' } }, source)).rejects.toThrow(
|
||||
LOGO_MISSING_PUBLISH,
|
||||
);
|
||||
await expect(loadLogoAsset('zw1', 'light')).resolves.toBeNull(); // auch das helle Bild ist weg: es wird neu geprüft
|
||||
});
|
||||
|
||||
it('eine Karte ohne Logo vergisst und holt nichts', async () => {
|
||||
const out = await renderForGallery({ ...card, config: { ...DEFAULT_CONFIG, logo: null } }, source);
|
||||
expect(out.png.type).toBe('image/png');
|
||||
expect(mocks.getLogo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('„Nur Bild“ (image) zeichnet nie ein Logo: auch ein inzwischen ausgeblendetes Logo hindert das Veröffentlichen nicht', async () => {
|
||||
await loadLogoAsset('zw1', 'light');
|
||||
mocks.getLogo.mockResolvedValue(logoRow({ hidden: true }));
|
||||
const out = await renderForGallery(withLogo('zw1', 'image'), source);
|
||||
expect(out.png.type).toBe('image/png');
|
||||
expect(mocks.exportCard).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertLogoAvailable', () => {
|
||||
const config = (template: TemplateId) => ({ ...DEFAULT_CONFIG, template, logo: 'abc123' });
|
||||
const assets = (logoMissing: boolean) => ({ image: null, logo: null, brand: null, logoMissing });
|
||||
|
||||
Reference in new issue
Block a user