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
@@ -23,6 +23,8 @@ const mocks = vi.hoisted(() => ({
|
||||
publishSavedCard: vi.fn(),
|
||||
withdrawCard: vi.fn(),
|
||||
myPublications: vi.fn(),
|
||||
getLogo: vi.fn(),
|
||||
loadLogoBlob: vi.fn(),
|
||||
authListeners: new Set<(user: User | null) => void>(),
|
||||
}));
|
||||
|
||||
@@ -67,9 +69,13 @@ vi.mock('../community/api', async (importOriginal) => ({
|
||||
withdrawCard: mocks.withdrawCard,
|
||||
}));
|
||||
vi.mock('../community/gallery', () => ({ myPublications: mocks.myPublications }));
|
||||
// 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 }));
|
||||
|
||||
import { ApiError } from '../community/api';
|
||||
import { LOGO_MISSING_PUBLISH } from '../community/publish';
|
||||
import { forgetLogo, loadLogoAsset } from '../logos/logo-asset';
|
||||
import type { CommunityLogo } from '../logos/logos';
|
||||
import { mountCardsPage } from './cards-page';
|
||||
|
||||
const user: User = { $id: 'u1', email: 'a@b.de' };
|
||||
@@ -377,6 +383,78 @@ describe('Meine Karten: Herunterladen mit fehlendem Logo', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meine Karten: Herunterladen und der Zwischenspeicher der Logos (ein ausgeblendetes Logo darf nicht ins PNG)', () => {
|
||||
const image = { source: {}, w: 3000, h: 2000 };
|
||||
const png = new Blob(['png'], { type: 'image/png' });
|
||||
const logoRow = (patch: Partial<CommunityLogo> = {}): CommunityLogo => ({
|
||||
id: 'zw2',
|
||||
name: 'Mein Logo',
|
||||
displayName: 'Kenso',
|
||||
fileId: 'zw2',
|
||||
ownerId: 'u1',
|
||||
mono: false,
|
||||
likeCount: 0,
|
||||
hidden: false,
|
||||
reportCount: 0,
|
||||
createdAt: '2026-10-04T10:00:00.000Z',
|
||||
...patch,
|
||||
});
|
||||
const msg = () => tileFor('a').querySelector('.card-msg')!;
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.list.mockResolvedValue([card('a', 'Alpha', { config: { ...structuredClone(DEFAULT_CONFIG), template: 'classic', logo: 'zw2' } })]);
|
||||
mocks.loadImage.mockResolvedValue(new Blob(['i'], { type: 'image/jpeg' }));
|
||||
mocks.decodeImage.mockResolvedValue(image);
|
||||
mocks.exportCard.mockResolvedValue(png);
|
||||
mocks.getLogo.mockResolvedValue(logoRow());
|
||||
mocks.loadLogoBlob.mockResolvedValue(new Blob(['logo'], { type: 'image/png' }));
|
||||
// Wie der echte Lader (card/render.ts: loadCardAssets), mit dem echten Zwischenspeicher der Logos darunter.
|
||||
mocks.loadCardAssets.mockImplementation(async (config: { logo: string | null; details: 'light' | 'dark' }, img: unknown) => {
|
||||
const logo = config.logo ? await loadLogoAsset(config.logo, config.details) : null;
|
||||
return { image: img, logo, brand: null, logoMissing: Boolean(config.logo) && logo === null };
|
||||
});
|
||||
vi.stubGlobal('createImageBitmap', async () => ({ width: 200, height: 100 }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
forgetLogo('zw2');
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
async function download(): Promise<void> {
|
||||
mount();
|
||||
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
|
||||
button(tileFor('a'), 'Herunterladen').click();
|
||||
}
|
||||
|
||||
it('ein Logo, das nach dem ersten Laden (Editor) ausgeblendet wurde, wird vor dem Download neu geprüft: Meldung an der Kachel, kein PNG', async () => {
|
||||
await expect(loadLogoAsset('zw2', 'light')).resolves.not.toBeNull(); // der Editor hat es geladen: Zeile und Bild stecken im Zwischenspeicher
|
||||
mocks.getLogo.mockResolvedValue(logoRow({ hidden: true })); // dann wurde es ausgeblendet
|
||||
await download();
|
||||
await vi.waitFor(() => expect(msg().textContent).toBe(LOGO_MISSING_PUBLISH));
|
||||
expect(mocks.getLogo).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||
expect(mocks.saveBlobAs).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ein weiterhin sichtbares Logo wird mit frisch geholter Zeile und Datei heruntergeladen', async () => {
|
||||
await expect(loadLogoAsset('zw2', 'light')).resolves.not.toBeNull();
|
||||
await download();
|
||||
await vi.waitFor(() => expect(mocks.saveBlobAs).toHaveBeenCalledWith(png, 'aircard-alpha.png'));
|
||||
expect(mocks.getLogo).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.loadLogoBlob).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.exportCard.mock.calls[0][1]).toMatchObject({ logo: { w: 200, h: 100 } });
|
||||
expect(msg().textContent).toBe('');
|
||||
});
|
||||
|
||||
it('eine Karte ohne Logo holt nichts vom Server', async () => {
|
||||
mocks.list.mockResolvedValue([card('a', 'Alpha', { config: { ...structuredClone(DEFAULT_CONFIG), template: 'classic', logo: null } })]);
|
||||
await download();
|
||||
await vi.waitFor(() => expect(mocks.saveBlobAs).toHaveBeenCalledWith(png, 'aircard-alpha.png'));
|
||||
expect(mocks.getLogo).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meine Karten: Sitzung weg, Appwrite antwortet mit 404 statt 401', () => {
|
||||
const fileGone = () => Object.assign(new Error('File not found'), { code: 404, type: 'storage_file_not_found' });
|
||||
const rowGone = () => Object.assign(new Error('Row not found'), { code: 404, type: 'row_not_found' });
|
||||
|
||||
Reference in new issue
Block a user