fix(community): Pixelgrenze beim Anzeigen, exakter Tag-Filter, unsichtbare Zeichen
- Gemeinsamer Helfer loadGalleryImageChecked/checkGalleryImage (gallery.ts): lädt das Bild über new Image(), lehnt über 4 Megapixel oder nicht dekodierbare Dateien ab und liefert eine fertige Object-URL (der Aufrufer gibt sie frei). Raster -> „Keine Vorschau“ (auch bei <img>-Fehler), Hero -> Beispielmotiv, Detailseite -> „Das Bild konnte nicht geladen werden.“ Verspätet eintreffende URLs werden sofort freigegeben. - Tag-Filter sucht ",<tag>," (der Server speichert ",a,b,"): „gold“ trifft nicht mehr „marigold“. - Veröffentlichen-Dialog lehnt dieselben unsichtbaren Zeichen ab wie die API (C1, Nullbreite, Bidi, BOM). - Das Limit der API (429 „schon 20 Karten veröffentlicht“) gilt als eindeutige Ablehnung: die hochgeladenen Dateien werden aufgeräumt. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5500495443
commit
c62075f2d4
12 files changed
+435
-66
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { User } from '../account/auth';
|
||||
import { ApiError, type ReportReason } from '../community/api';
|
||||
import type { GalleryCard, GalleryReport } from '../community/gallery';
|
||||
import type { CheckedImage, GalleryCard, GalleryReport } from '../community/gallery';
|
||||
|
||||
// gallery.ts, api.ts und auth.ts hängen am Appwrite-Client; die Seite bekommt alles über ihre Abhängigkeiten.
|
||||
vi.mock('../account/appwrite', () => ({
|
||||
@@ -43,6 +43,8 @@ const settle = async () => {
|
||||
for (let i = 0; i < 40; i++) await Promise.resolve();
|
||||
};
|
||||
const blob = () => new Blob(['png'], { type: 'image/png' });
|
||||
/** Ein geprüftes Bild, wie es loadGalleryImageChecked liefert: Blob plus fertige Object-URL. */
|
||||
const image = (b: Blob = blob()): CheckedImage => ({ blob: b, url: `blob:detail-${++urlCounter}` });
|
||||
|
||||
const admin: User = { $id: 'a1', email: 'kenso@example.com', labels: ['admin'] };
|
||||
const owner: User = { $id: 'u1', email: 'u@example.com', labels: [] };
|
||||
@@ -58,7 +60,7 @@ let urlCounter: number;
|
||||
const revoked: string[] = [];
|
||||
const getCard = vi.fn<(id: string) => Promise<GalleryCard | null>>();
|
||||
const listReports = vi.fn<(id: string) => Promise<GalleryReport[]>>();
|
||||
const loadImage = vi.fn<(c: GalleryCard, size: 'full' | 'preview') => Promise<Blob>>();
|
||||
const loadImage = vi.fn<(c: GalleryCard, size: 'full' | 'preview') => Promise<CheckedImage>>();
|
||||
const reportCard = vi.fn<(id: string, reason: ReportReason) => Promise<{ schonGemeldet: boolean }>>();
|
||||
const withdrawCard = vi.fn<(id: string) => Promise<void>>();
|
||||
const setVisibility = vi.fn<(id: string, visible: boolean) => Promise<void>>();
|
||||
@@ -115,7 +117,7 @@ beforeEach(() => {
|
||||
}
|
||||
getCard.mockResolvedValue(card());
|
||||
listReports.mockResolvedValue([]);
|
||||
loadImage.mockImplementation(async () => blob());
|
||||
loadImage.mockImplementation(async () => image());
|
||||
withdrawCard.mockResolvedValue(undefined);
|
||||
setVisibility.mockResolvedValue(undefined);
|
||||
getUser.mockResolvedValue(null);
|
||||
@@ -123,7 +125,6 @@ beforeEach(() => {
|
||||
authListeners = new Set();
|
||||
urlCounter = 0;
|
||||
revoked.length = 0;
|
||||
URL.createObjectURL = vi.fn(() => `blob:detail-${++urlCounter}`);
|
||||
URL.revokeObjectURL = vi.fn((url: string) => {
|
||||
revoked.push(url);
|
||||
});
|
||||
@@ -276,6 +277,31 @@ describe('Detailseite: Karte', () => {
|
||||
expect(saveBlobAs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('ein von der Pixelprüfung abgelehntes Bild (zu groß, nicht dekodierbar) zeigt den Hinweis statt eines Bildes', async () => {
|
||||
// so lehnt loadGalleryImageChecked ab: es wirft, noch bevor ein <img> entsteht
|
||||
loadImage.mockRejectedValue(new Error('Das Bild konnte nicht geladen werden.'));
|
||||
await mount();
|
||||
expect(q('.detail-card')?.textContent).toContain('Das Bild konnte nicht geladen werden.');
|
||||
expect(q('.detail-card img')).toBeNull();
|
||||
expect(q('h1')?.textContent).toBe('Mitternachtsblau'); // die Angaben und Knöpfe (Melden!) bleiben bedienbar
|
||||
expect(buttonByText('Melden')).toBeDefined();
|
||||
// auch Herunterladen bekommt das Bild nicht
|
||||
buttonByText('Herunterladen')!.click();
|
||||
await settle();
|
||||
expect(saveBlobAs).not.toHaveBeenCalled();
|
||||
expect(errorText()).toBe('Das Bild konnte nicht geladen werden.');
|
||||
});
|
||||
|
||||
it('das Bild zum Herunterladen kommt aus der Prüfung; eine dabei angelegte URL wird sofort freigegeben', async () => {
|
||||
await mount();
|
||||
q('.detail-card img')!.dispatchEvent(new Event('error')); // kein Bild im Speicher
|
||||
revoked.length = 0;
|
||||
buttonByText('Herunterladen')!.click();
|
||||
await settle();
|
||||
expect(saveBlobAs).toHaveBeenCalledTimes(1);
|
||||
expect(revoked).toEqual(['blob:detail-2']);
|
||||
});
|
||||
|
||||
it('„Nochmal versuchen“ beim Bild lädt es erneut', async () => {
|
||||
loadImage.mockRejectedValueOnce(new TypeError('offline'));
|
||||
await mount();
|
||||
@@ -311,17 +337,18 @@ describe('Detailseite: Karte', () => {
|
||||
expect(q('h1')).toBeNull(); // die Karte wird nicht mehr aufgebaut
|
||||
|
||||
// Bild kommt erst nach dem Verlassen
|
||||
let finishImage!: (b: Blob) => void;
|
||||
let finishImage!: (i: CheckedImage) => void;
|
||||
getCard.mockResolvedValue(card());
|
||||
loadImage.mockReturnValue(new Promise((r) => (finishImage = r)));
|
||||
unmount = mountCommunityPage(root, 'g1', deps());
|
||||
await settle();
|
||||
unmount();
|
||||
unmount = null;
|
||||
const created = urlCounter;
|
||||
finishImage(blob());
|
||||
revoked.length = 0;
|
||||
finishImage({ blob: blob(), url: 'blob:detail-late' });
|
||||
await settle();
|
||||
expect(urlCounter).toBe(created);
|
||||
expect(revoked).toEqual(['blob:detail-late']); // die verspätete URL wird sofort freigegeben
|
||||
expect(q('img')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -419,7 +446,7 @@ describe('Detailseite: Aufräumen', () => {
|
||||
describe('Detailseite: Herunterladen und Vorlage', () => {
|
||||
it('Herunterladen speichert das volle PNG als aircard-<titel-slug>.png', async () => {
|
||||
const full = blob();
|
||||
loadImage.mockResolvedValue(full);
|
||||
loadImage.mockImplementation(async () => image(full));
|
||||
getCard.mockResolvedValue(card({ title: 'Grüner Wald!' }));
|
||||
await mount();
|
||||
buttonByText('Herunterladen')!.click();
|
||||
|
||||
Reference in new issue
Block a user