Skip to content

Commit 07b4053

Browse files
authored
Merge pull request #160 from mean-weasel/fix-safari-complex-screenshots
Fix Safari freezes on complex screenshot pages
2 parents 1016f07 + 324362f commit 07b4053

4 files changed

Lines changed: 106 additions & 8 deletions

File tree

e2e/widget.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2991,6 +2991,36 @@ test.describe('Screenshot Crash Prevention (#67)', () => {
29912991
await expect(notice).toBeVisible();
29922992
});
29932993

2994+
test('hides full-page and area capture earlier for complex Safari pages', async ({ page }) => {
2995+
await mockHtmlToImage(
2996+
page,
2997+
"function() { throw new Error('html-to-image should not run for Safari complex-page options'); }"
2998+
);
2999+
await page.addInitScript(() => {
3000+
Object.defineProperty(navigator, 'mediaDevices', {
3001+
value: {},
3002+
configurable: true,
3003+
});
3004+
Object.defineProperty(navigator, 'userAgent', {
3005+
get: () =>
3006+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/616.1.16 (KHTML, like Gecko) Version/26.4 Safari/616.1.16',
3007+
configurable: true,
3008+
});
3009+
});
3010+
await page.goto('/test/complex-dom.html?nodes=4000');
3011+
3012+
const nodeCount = await page.evaluate(() => document.body.querySelectorAll('*').length);
3013+
expect(nodeCount).toBeGreaterThanOrEqual(3000);
3014+
expect(nodeCount).toBeLessThan(10000);
3015+
3016+
const host = await navigateToScreenshotOptions(page);
3017+
3018+
await expect(host.locator('css=[data-action="element"]')).toBeVisible();
3019+
await expect(host.locator('css=[data-action="capture"]')).not.toBeAttached();
3020+
await expect(host.locator('css=[data-action="area"]')).not.toBeAttached();
3021+
await expect(host.locator('css=p >> text=too complex')).toBeVisible();
3022+
});
3023+
29943024
test('offers native viewport capture on very complex secure-context pages', async ({ page }) => {
29953025
const payloads = await trackFeedbackPayloads(page);
29963026
await mockHtmlToImage(

src/widget/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
FULL_PAGE_DISABLE_THRESHOLD,
3-
getDomNodeCount,
4-
getRedactionCount,
5-
isFullPageDisabled,
6-
} from './screenshot';
1+
import { getDomNodeCount, getRedactionCount, isFullPageDisabled } from './screenshot';
72
import { runScreenshotCaptureFlow } from './capture-flow';
83
import { injectStyles, createModal, showSuccessModal } from './ui';
94
import {
@@ -1153,7 +1148,7 @@ async function submitFeedback(root: HTMLElement, config: WidgetConfig, data: Fee
11531148
timestamp: new Date().toISOString(),
11541149
elementSelector: data.elementSelector,
11551150
domNodeCount,
1156-
fullPageDisabled: domNodeCount >= FULL_PAGE_DISABLE_THRESHOLD,
1151+
fullPageDisabled: isFullPageDisabled(),
11571152
// Parsed system info
11581153
browser: systemInfo.browser,
11591154
os: systemInfo.os,

src/widget/screenshot.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const DOM_COMPLEXITY_THRESHOLD = 3_000;
99
const TRANSPARENT_IMAGE_PLACEHOLDER =
1010
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
1111
export const FULL_PAGE_DISABLE_THRESHOLD = 10_000;
12+
export const SAFARI_FULL_PAGE_DISABLE_THRESHOLD = DOM_COMPLEXITY_THRESHOLD;
1213

1314
type DisplayMediaOptionsWithCurrentTab = DisplayMediaStreamOptions & {
1415
preferCurrentTab?: boolean;
@@ -30,7 +31,20 @@ export function getDomNodeCount(): number {
3031
}
3132

3233
export function isFullPageDisabled(): boolean {
33-
return getDomNodeCount() >= FULL_PAGE_DISABLE_THRESHOLD;
34+
return getDomNodeCount() >= getFullPageDisableThreshold();
35+
}
36+
37+
export function getFullPageDisableThreshold(userAgent = navigator.userAgent): number {
38+
return isSafariBrowser(userAgent)
39+
? SAFARI_FULL_PAGE_DISABLE_THRESHOLD
40+
: FULL_PAGE_DISABLE_THRESHOLD;
41+
}
42+
43+
export function isSafariBrowser(userAgent = navigator.userAgent): boolean {
44+
return (
45+
/Safari\//.test(userAgent) &&
46+
!/(Chrome|Chromium|CriOS|FxiOS|Edg|EdgiOS|OPR|Opera)\//.test(userAgent)
47+
);
3448
}
3549

3650
export function getPixelRatio(isFullPage: boolean, screenshotScale?: number): number {

test/cropScreenshot.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
33
import {
44
getPixelRatio,
55
getDomNodeCount,
6+
getFullPageDisableThreshold,
67
isFullPageDisabled,
8+
isSafariBrowser,
79
FULL_PAGE_DISABLE_THRESHOLD,
10+
SAFARI_FULL_PAGE_DISABLE_THRESHOLD,
811
cropScreenshot,
912
canCaptureViewportNatively,
1013
beginViewportCapture,
@@ -109,6 +112,62 @@ describe('isFullPageDisabled', () => {
109112
);
110113
expect(isFullPageDisabled()).toBe(false);
111114
});
115+
116+
it('uses the lower Safari threshold to avoid expensive full-page captures', () => {
117+
const elements = new Array(SAFARI_FULL_PAGE_DISABLE_THRESHOLD).fill(
118+
document.createElement('div')
119+
);
120+
vi.spyOn(document.body, 'querySelectorAll').mockReturnValue(
121+
elements as unknown as NodeListOf<Element>
122+
);
123+
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
124+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/616.1.16 (KHTML, like Gecko) Version/26.4 Safari/616.1.16'
125+
);
126+
127+
expect(isFullPageDisabled()).toBe(true);
128+
});
129+
130+
it('keeps Chromium on the higher full-page threshold', () => {
131+
const elements = new Array(SAFARI_FULL_PAGE_DISABLE_THRESHOLD).fill(
132+
document.createElement('div')
133+
);
134+
vi.spyOn(document.body, 'querySelectorAll').mockReturnValue(
135+
elements as unknown as NodeListOf<Element>
136+
);
137+
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
138+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
139+
);
140+
141+
expect(isFullPageDisabled()).toBe(false);
142+
});
143+
});
144+
145+
describe('browser-specific full-page thresholds', () => {
146+
it('detects Safari without matching Chrome-style user agents', () => {
147+
expect(
148+
isSafariBrowser(
149+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/616.1.16 (KHTML, like Gecko) Version/26.4 Safari/616.1.16'
150+
)
151+
).toBe(true);
152+
expect(
153+
isSafariBrowser(
154+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
155+
)
156+
).toBe(false);
157+
});
158+
159+
it('returns the Safari complexity threshold for Safari only', () => {
160+
expect(
161+
getFullPageDisableThreshold(
162+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/616.1.16 (KHTML, like Gecko) Version/26.4 Safari/616.1.16'
163+
)
164+
).toBe(SAFARI_FULL_PAGE_DISABLE_THRESHOLD);
165+
expect(
166+
getFullPageDisableThreshold(
167+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
168+
)
169+
).toBe(FULL_PAGE_DISABLE_THRESHOLD);
170+
});
112171
});
113172

114173
describe('cropScreenshot', () => {

0 commit comments

Comments
 (0)