bf4384ad73
html-to-image laedt jedes <img> per fetch nach; ein einziges Bild ohne CORS (z. B. ein direkt von der Website geholtes Favoriten-Symbol) liess die ganze Aufnahme scheitern, im Dialog blieb das Haekchen "Bildschirmfoto beifuegen" gesperrt. Nicht ladbare Bilder werden jetzt zum transparenten Pixel; scheitert die Aufnahme trotzdem, folgt ein zweiter Versuch ohne Bilder und Rahmen. Im Linux-Client 1.3.1 nachgestellt und nach dem Fix gegengeprueft. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { captureScreenshot, sendBugReport } from './bug-report-api';
|
|
|
|
const { mockToPng } = vi.hoisted(() => ({ mockToPng: vi.fn() }));
|
|
vi.mock('html-to-image', () => ({ toPng: mockToPng }));
|
|
|
|
/**
|
|
* bug-report-api.test — NEU (quick-260918-gza). Bisher pruefte nur der
|
|
* Komponententest (`bug-report-button.test.tsx`) die FormData von
|
|
* `sendBugReport` — die reinen Nutzlastfelder (insbesondere die vier neuen
|
|
* Herkunftsfelder) gehoeren aber an die Funktion selbst, unabhaengig vom
|
|
* Dialog. Drei Tests: Desktop-Nutzlast, Browser-Nutzlast, Netzwerk-/
|
|
* HTTP-Fehler.
|
|
*/
|
|
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }));
|
|
|
|
const basePayload = {
|
|
description: 'x',
|
|
page: '/a',
|
|
webVersion: 'v1',
|
|
webChannel: 'beta',
|
|
webCommit: 'c',
|
|
userAgent: 'UA',
|
|
viewport: '1x1',
|
|
clientTime: 't',
|
|
errors: ['e1', 'e2'],
|
|
screenshot: null,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockFetch.mockReset();
|
|
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
|
|
vi.stubGlobal('fetch', mockFetch);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('sendBugReport (quick-260918-gza)', () => {
|
|
it('Test 1: Desktop-Nutzlast — clientKind/clientOs/clientVersion/clientCommit im FormData, errors mehrfach, kein screenshot-Feld', async () => {
|
|
const result = await sendBugReport({
|
|
...basePayload,
|
|
clientKind: 'desktop',
|
|
clientOs: 'windows',
|
|
clientVersion: '1.2.0',
|
|
clientCommit: 'a6d1a64',
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
expect(String(url).endsWith('/bug-reports')).toBe(true);
|
|
expect(init.credentials).toBe('include');
|
|
const body = init.body as FormData;
|
|
expect(body.get('clientKind')).toBe('desktop');
|
|
expect(body.get('clientOs')).toBe('windows');
|
|
expect(body.get('clientVersion')).toBe('1.2.0');
|
|
expect(body.get('clientCommit')).toBe('a6d1a64');
|
|
expect(body.getAll('errors')).toEqual(['e1', 'e2']);
|
|
expect(body.has('screenshot')).toBe(false);
|
|
});
|
|
|
|
it('Test 2: Browser-Nutzlast — die drei uebrigen Client-Felder sind vorhanden, aber leer (nicht weggelassen)', async () => {
|
|
await sendBugReport({
|
|
...basePayload,
|
|
clientKind: 'browser',
|
|
clientOs: '',
|
|
clientVersion: '',
|
|
clientCommit: '',
|
|
});
|
|
|
|
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData;
|
|
expect(body.get('clientKind')).toBe('browser');
|
|
expect(body.get('clientOs')).toBe('');
|
|
expect(body.get('clientVersion')).toBe('');
|
|
expect(body.get('clientCommit')).toBe('');
|
|
});
|
|
|
|
it('Test 3: Netzwerkfehler -> { ok: false, status: 0 }; HTTP-Fehler -> { ok: false, status }', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('offline'));
|
|
await expect(
|
|
sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }),
|
|
).resolves.toEqual({ ok: false, status: 0 });
|
|
|
|
mockFetch.mockResolvedValueOnce(new Response('', { status: 429 }));
|
|
await expect(
|
|
sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }),
|
|
).resolves.toEqual({ ok: false, status: 429 });
|
|
});
|
|
});
|
|
|
|
describe('captureScreenshot (Bild von fremder Adresse)', () => {
|
|
beforeEach(() => {
|
|
mockToPng.mockReset();
|
|
});
|
|
|
|
it('ersetzt nicht ladbare Bilder statt die Aufnahme abzubrechen', async () => {
|
|
mockToPng.mockResolvedValue('data:image/png;base64,AAA');
|
|
|
|
await expect(captureScreenshot()).resolves.toBe('data:image/png;base64,AAA');
|
|
|
|
const opts = mockToPng.mock.calls[0][1];
|
|
expect(opts.imagePlaceholder).toMatch(/^data:image\/png;base64,/);
|
|
expect(opts.onImageErrorHandler()).toBeUndefined();
|
|
});
|
|
|
|
it('versucht es ein zweites Mal ohne Bilder und Rahmen, wenn der erste Versuch scheitert', async () => {
|
|
mockToPng
|
|
.mockRejectedValueOnce(new Error('tainted'))
|
|
.mockResolvedValueOnce('data:image/png;base64,BBB');
|
|
|
|
await expect(captureScreenshot()).resolves.toBe('data:image/png;base64,BBB');
|
|
|
|
expect(mockToPng).toHaveBeenCalledTimes(2);
|
|
const filter = mockToPng.mock.calls[1][1].filter as (n: Node) => boolean;
|
|
expect(filter(document.createElement('img'))).toBe(false);
|
|
expect(filter(document.createElement('iframe'))).toBe(false);
|
|
expect(filter(document.createElement('div'))).toBe(true);
|
|
const ignored = document.createElement('div');
|
|
ignored.dataset.bugReportIgnore = 'true';
|
|
expect(filter(ignored)).toBe(false);
|
|
});
|
|
|
|
it('liefert null, wenn auch der zweite Versuch scheitert', async () => {
|
|
mockToPng.mockRejectedValue(new Error('kaputt'));
|
|
|
|
await expect(captureScreenshot()).resolves.toBeNull();
|
|
});
|
|
});
|