@@ -2202,6 +2202,17 @@ test.describe('Screenshot Crash Prevention (#67)', () => {
22022202 }` ;
22032203 }
22042204
2205+ function mockGetDisplayMedia ( page : Page , body : string ) {
2206+ return page . addInitScript ( `
2207+ Object.defineProperty(navigator, 'mediaDevices', {
2208+ value: {
2209+ getDisplayMedia: ${ body }
2210+ },
2211+ configurable: true
2212+ });
2213+ ` ) ;
2214+ }
2215+
22052216 function reporterLikePng (
22062217 variant : 'preview-size' | 'small-wide-area' | 'annotation-style' | 'undo'
22072218 ) {
@@ -2703,10 +2714,16 @@ test.describe('Screenshot Crash Prevention (#67)', () => {
27032714
27042715 // --- Full-page disable threshold (10k+ nodes) ---
27052716
2706- test ( 'hides Full Page and Select Area buttons on very complex pages (>10k nodes) ' , async ( {
2717+ test ( 'hides Full Page and Select Area buttons on very complex pages without native viewport capture ' , async ( {
27072718 page,
27082719 } ) => {
27092720 await mockHtmlToImage ( page , spyToPng ( ) ) ;
2721+ await page . addInitScript ( ( ) => {
2722+ Object . defineProperty ( navigator , 'mediaDevices' , {
2723+ value : { } ,
2724+ configurable : true ,
2725+ } ) ;
2726+ } ) ;
27102727 await page . goto ( '/test/complex-dom.html?nodes=12000' ) ;
27112728
27122729 const nodeCount = await page . evaluate ( ( ) => document . body . querySelectorAll ( '*' ) . length ) ;
@@ -2726,6 +2743,177 @@ test.describe('Screenshot Crash Prevention (#67)', () => {
27262743 await expect ( notice ) . toBeVisible ( ) ;
27272744 } ) ;
27282745
2746+ test ( 'offers native viewport capture on very complex secure-context pages' , async ( { page } ) => {
2747+ const payloads = await trackFeedbackPayloads ( page ) ;
2748+ await mockHtmlToImage (
2749+ page ,
2750+ "function() { throw new Error('html-to-image should not run for viewport capture'); }"
2751+ ) ;
2752+ await mockGetDisplayMedia (
2753+ page ,
2754+ `function(opts) {
2755+ window.__viewportCaptureCalls = (window.__viewportCaptureCalls || 0) + 1;
2756+ window.__viewportCaptureUserActivation = navigator.userActivation.isActive;
2757+ window.__viewportCaptureOpts = opts;
2758+ var canvas = document.createElement('canvas');
2759+ canvas.width = 960;
2760+ canvas.height = 540;
2761+ var ctx = canvas.getContext('2d');
2762+ ctx.fillStyle = '#101827';
2763+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2764+ ctx.fillStyle = '#22d3ee';
2765+ ctx.fillRect(40, 40, 880, 120);
2766+ ctx.fillStyle = '#ffffff';
2767+ ctx.font = 'bold 42px sans-serif';
2768+ ctx.fillText('Native viewport capture', 80, 115);
2769+ var stream = canvas.captureStream();
2770+ var track = stream.getVideoTracks()[0];
2771+ var originalStop = track.stop.bind(track);
2772+ track.getSettings = function() { return { displaySurface: 'browser' }; };
2773+ track.stop = function() {
2774+ window.__viewportTrackStops = (window.__viewportTrackStops || 0) + 1;
2775+ originalStop();
2776+ };
2777+ return Promise.resolve(stream);
2778+ }`
2779+ ) ;
2780+ await page . goto ( '/test/complex-dom.html?nodes=12000' ) ;
2781+
2782+ const nodeCount = await page . evaluate ( ( ) => document . body . querySelectorAll ( '*' ) . length ) ;
2783+ expect ( nodeCount ) . toBeGreaterThanOrEqual ( 10000 ) ;
2784+
2785+ const host = await navigateToScreenshotOptions ( page ) ;
2786+
2787+ const viewportBtn = host . locator ( 'css=[data-action="viewport"]' ) ;
2788+ await expect ( viewportBtn ) . toBeVisible ( ) ;
2789+ await expect ( viewportBtn ) . toHaveText ( 'Capture Viewport' ) ;
2790+ await expect ( host . locator ( 'css=[data-action="capture"]' ) ) . not . toBeAttached ( ) ;
2791+ await expect ( host . locator ( 'css=[data-action="area"]' ) ) . not . toBeAttached ( ) ;
2792+ await expect ( host . locator ( 'css=p >> text=visible viewport' ) ) . toBeVisible ( ) ;
2793+
2794+ await viewportBtn . click ( ) ;
2795+
2796+ await expect ( host . locator ( 'css=.bd-modal--annotator' ) ) . toBeVisible ( { timeout : 10000 } ) ;
2797+ await expect ( host . locator ( 'css=#annotation-canvas canvas' ) ) . toBeVisible ( ) ;
2798+ await expect
2799+ . poll ( ( ) =>
2800+ page . evaluate (
2801+ ( ) => ( window as Window & { __viewportCaptureCalls ?: number } ) . __viewportCaptureCalls || 0
2802+ )
2803+ )
2804+ . toBe ( 1 ) ;
2805+ await expect
2806+ . poll ( ( ) =>
2807+ page . evaluate (
2808+ ( ) => ( window as Window & { __viewportTrackStops ?: number } ) . __viewportTrackStops || 0
2809+ )
2810+ )
2811+ . toBe ( 1 ) ;
2812+ const viewportCapture = await page . evaluate ( ( ) => {
2813+ const win = window as Window & {
2814+ __viewportCaptureOpts ?: DisplayMediaStreamOptions & { preferCurrentTab ?: boolean } ;
2815+ __viewportCaptureUserActivation ?: boolean ;
2816+ } ;
2817+ return {
2818+ opts : win . __viewportCaptureOpts ,
2819+ userActivation : win . __viewportCaptureUserActivation ,
2820+ } ;
2821+ } ) ;
2822+ expect ( viewportCapture . userActivation ) . toBe ( true ) ;
2823+ expect ( viewportCapture . opts ) . toEqual ( {
2824+ video : { displaySurface : 'browser' } ,
2825+ audio : false ,
2826+ preferCurrentTab : true ,
2827+ } ) ;
2828+ const captureOpts = await page . evaluate (
2829+ ( ) => ( window as Window & { __captureOpts ?: unknown } ) . __captureOpts
2830+ ) ;
2831+ expect ( captureOpts ) . toBeUndefined ( ) ;
2832+
2833+ await host . locator ( 'css=[data-action="done"]' ) . click ( ) ;
2834+ await expect ( host . locator ( 'css=.bd-success-icon' ) ) . toBeVisible ( { timeout : 10000 } ) ;
2835+ expect ( payloads ) . toHaveLength ( 1 ) ;
2836+ expect ( payloads [ 0 ] . screenshot ) . toEqual ( expect . stringMatching ( / ^ d a t a : i m a g e \/ p n g ; b a s e 6 4 , / ) ) ;
2837+ } ) ;
2838+
2839+ test ( 'retries native viewport capture from the capture error modal' , async ( { page } ) => {
2840+ await mockGetDisplayMedia (
2841+ page ,
2842+ `function() {
2843+ window.__viewportCaptureCalls = (window.__viewportCaptureCalls || 0) + 1;
2844+ if (window.__viewportCaptureCalls === 1) {
2845+ return Promise.reject(new Error('Permission denied'));
2846+ }
2847+ var canvas = document.createElement('canvas');
2848+ canvas.width = 2;
2849+ canvas.height = 2;
2850+ canvas.getContext('2d').fillRect(0, 0, 2, 2);
2851+ var stream = canvas.captureStream();
2852+ stream.getVideoTracks()[0].getSettings = function() { return { displaySurface: 'browser' }; };
2853+ return Promise.resolve(stream);
2854+ }`
2855+ ) ;
2856+ await page . goto ( '/test/complex-dom.html?nodes=12000' ) ;
2857+
2858+ const host = await navigateToScreenshotOptions ( page ) ;
2859+ await host . locator ( 'css=[data-action="viewport"]' ) . click ( ) ;
2860+
2861+ const errorText = host . locator ( 'css=.bd-error-message__text' ) ;
2862+ await expect ( errorText ) . toBeVisible ( { timeout : 5000 } ) ;
2863+ await expect
2864+ . poll ( ( ) =>
2865+ page . evaluate (
2866+ ( ) => ( window as Window & { __viewportCaptureCalls ?: number } ) . __viewportCaptureCalls || 0
2867+ )
2868+ )
2869+ . toBe ( 1 ) ;
2870+
2871+ await host . locator ( 'css=[data-action="retry"]' ) . click ( ) ;
2872+
2873+ await expect ( host . locator ( 'css=.bd-modal--annotator' ) ) . toBeVisible ( { timeout : 10000 } ) ;
2874+ await expect
2875+ . poll ( ( ) =>
2876+ page . evaluate (
2877+ ( ) => ( window as Window & { __viewportCaptureCalls ?: number } ) . __viewportCaptureCalls || 0
2878+ )
2879+ )
2880+ . toBe ( 2 ) ;
2881+ } ) ;
2882+
2883+ test ( 'rejects non-browser native capture surfaces before annotation' , async ( { page } ) => {
2884+ await mockGetDisplayMedia (
2885+ page ,
2886+ `function() {
2887+ var canvas = document.createElement('canvas');
2888+ canvas.width = 2;
2889+ canvas.height = 2;
2890+ var stream = canvas.captureStream();
2891+ var track = stream.getVideoTracks()[0];
2892+ var originalStop = track.stop.bind(track);
2893+ track.getSettings = function() { return { displaySurface: 'monitor' }; };
2894+ track.stop = function() {
2895+ window.__viewportTrackStops = (window.__viewportTrackStops || 0) + 1;
2896+ originalStop();
2897+ };
2898+ return Promise.resolve(stream);
2899+ }`
2900+ ) ;
2901+ await page . goto ( '/test/complex-dom.html?nodes=12000' ) ;
2902+
2903+ const host = await navigateToScreenshotOptions ( page ) ;
2904+ await host . locator ( 'css=[data-action="viewport"]' ) . click ( ) ;
2905+
2906+ await expect ( host . locator ( 'css=.bd-error-message__text' ) ) . toBeVisible ( { timeout : 5000 } ) ;
2907+ await expect ( host . locator ( 'css=.bd-modal--annotator' ) ) . not . toBeAttached ( ) ;
2908+ await expect
2909+ . poll ( ( ) =>
2910+ page . evaluate (
2911+ ( ) => ( window as Window & { __viewportTrackStops ?: number } ) . __viewportTrackStops || 0
2912+ )
2913+ )
2914+ . toBe ( 1 ) ;
2915+ } ) ;
2916+
27292917 test ( 'remembers complex-page screenshot skip for issue #116 repeated reports' , async ( {
27302918 page,
27312919 } ) => {
0 commit comments