fix(bug-report): Bildschirmfoto scheitert nicht mehr an einem fremden Bild
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>
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { sendBugReport } from './bug-report-api';
|
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
|
* bug-report-api.test — NEU (quick-260918-gza). Bisher pruefte nur der
|
||||||
@@ -85,3 +88,42 @@ describe('sendBugReport (quick-260918-gza)', () => {
|
|||||||
).resolves.toEqual({ ok: false, status: 429 });
|
).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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -42,23 +42,50 @@ export function computeCaptureSize(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 1x1 transparentes PNG — Ersatz fuer Bilder, die sich nicht laden lassen. */
|
||||||
|
const TRANSPARENT_PIXEL =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||||
|
|
||||||
|
function isIgnoredNode(n: Node): boolean {
|
||||||
|
return n instanceof HTMLElement && n.dataset.bugReportIgnore === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nimmt die aktuelle Seite als PNG-Data-URL auf. Liefert `null` statt zu
|
* Nimmt die aktuelle Seite als PNG-Data-URL auf. Liefert `null` statt zu
|
||||||
* werfen: das Bild ist eine Beigabe, der Bericht geht auch ohne.
|
* werfen: das Bild ist eine Beigabe, der Bericht geht auch ohne.
|
||||||
|
*
|
||||||
|
* `html-to-image` laedt jedes `<img>` per `fetch` nach. Ein einziges Bild
|
||||||
|
* von fremder Adresse ohne CORS (z. B. ein direkt von der Website geholtes
|
||||||
|
* Favoriten-Symbol) liess bisher die GANZE Aufnahme scheitern — im Dialog
|
||||||
|
* blieb das Haekchen „Bildschirmfoto beifuegen“ dann gesperrt. Deshalb:
|
||||||
|
* nicht ladbare Bilder werden zum transparenten Pixel, und scheitert die
|
||||||
|
* Aufnahme trotzdem, folgt ein zweiter Versuch ganz ohne Bilder und Rahmen.
|
||||||
*/
|
*/
|
||||||
export async function captureScreenshot(): Promise<string | null> {
|
export async function captureScreenshot(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const node = document.body;
|
const node = document.body;
|
||||||
const size = computeCaptureSize(node.scrollWidth, node.scrollHeight);
|
const size = computeCaptureSize(node.scrollWidth, node.scrollHeight);
|
||||||
return await toPng(node, {
|
const base = {
|
||||||
pixelRatio: 1,
|
pixelRatio: 1,
|
||||||
skipFonts: true,
|
skipFonts: true,
|
||||||
cacheBust: true,
|
cacheBust: true,
|
||||||
canvasWidth: size.width,
|
canvasWidth: size.width,
|
||||||
canvasHeight: size.height,
|
canvasHeight: size.height,
|
||||||
filter: (n: Node) => !(n instanceof HTMLElement && n.dataset.bugReportIgnore === 'true'),
|
imagePlaceholder: TRANSPARENT_PIXEL,
|
||||||
|
onImageErrorHandler: () => undefined,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
return await toPng(node, { ...base, filter: (n: Node) => !isIgnoredNode(n) });
|
||||||
|
} catch {
|
||||||
|
return await toPng(node, {
|
||||||
|
...base,
|
||||||
|
filter: (n: Node) =>
|
||||||
|
!isIgnoredNode(n) &&
|
||||||
|
!(n instanceof HTMLImageElement) &&
|
||||||
|
!(n instanceof HTMLIFrameElement),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user