Skip to content

Commit ed5bf0e

Browse files
Implement multi-strategy full-page screenshot with CDP and Firefox native support
- Add Chrome DevTools Protocol (CDP) method for Chrome/Edge browsers Works in both headless and non-headless mode without screen size limits - Add Firefox native full-page screenshot support - Implement browser detection and capability checking - Use strategy pattern with automatic fallback to window resize method - Window resize method validates actual dimensions and warns on limitations - Update documentation to describe the three screenshot strategies - Resolves screen size limitation concerns in non-headless mode
1 parent 86402e9 commit ed5bf0e

File tree

1 file changed

+192
-11
lines changed

1 file changed

+192
-11
lines changed

src/SeleniumLibrary/keywords/screenshot.py

Lines changed: 192 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616
import os
17+
import base64
1718
from typing import Optional, Union
18-
from base64 import b64decode
1919

2020
from robot.utils import get_link_path
2121
from selenium.webdriver.remote.webelement import WebElement
@@ -204,9 +204,22 @@ def _capture_element_screen_to_log(self, element, return_val):
204204
def capture_fullpage_screenshot(self, filename: str = DEFAULT_FILENAME_FULLPAGE) -> str:
205205
"""Takes a screenshot of the entire page, including parts not visible in viewport.
206206
207-
This keyword is useful when you need to capture a long page that requires
208-
scrolling. It works by temporarily resizing the browser window to show
209-
the full page height, taking the screenshot, then restoring the original size.
207+
This keyword captures the full height and width of a web page, even if it extends
208+
beyond the current viewport. The implementation automatically selects the best
209+
available method based on the browser:
210+
211+
*Screenshot Methods (in order of preference):*
212+
213+
1. **Chrome DevTools Protocol (CDP)**: Used for Chrome, Edge, and Chromium browsers.
214+
Works in both headless and non-headless mode without screen size limitations.
215+
216+
2. **Firefox Native Method**: Used for Firefox browsers. Captures full page
217+
using the browser's built-in capability.
218+
219+
3. **Window Resize Method**: Fallback for other browsers. Temporarily resizes
220+
the browser window to match page dimensions. In non-headless mode, this may
221+
be limited by physical screen size, and a warning will be logged if the full
222+
page cannot be captured.
210223
211224
``filename`` argument specifies where to save the screenshot file.
212225
The directory can be set with `Set Screenshot Directory` keyword or
@@ -241,7 +254,27 @@ def capture_fullpage_screenshot(self, filename: str = DEFAULT_FILENAME_FULLPAGE)
241254
return self._capture_fullpage_screenshot_to_file(filename)
242255

243256
def _capture_fullpage_screenshot_to_file(self, filename):
244-
"""Save fullpage screenshot to file."""
257+
"""Save fullpage screenshot to file using best available method."""
258+
# Try CDP first (Chrome/Edge/Chromium) - works in both headless and non-headless
259+
if self._supports_cdp():
260+
result = self._capture_fullpage_via_cdp(filename)
261+
if result:
262+
self.debug("Full-page screenshot captured using Chrome DevTools Protocol")
263+
return result
264+
265+
# Try Firefox native method
266+
if self._supports_native_fullpage():
267+
result = self._capture_fullpage_via_firefox(filename)
268+
if result:
269+
self.debug("Full-page screenshot captured using Firefox native method")
270+
return result
271+
272+
# Fallback to resize method (works in headless mode for all browsers)
273+
self.debug("Using window resize method for full-page screenshot")
274+
return self._capture_fullpage_via_resize(filename)
275+
276+
def _capture_fullpage_via_resize(self, filename):
277+
"""Fallback method: Save fullpage screenshot by resizing window."""
245278
# Remember current window size so we can restore it later
246279
original_size = self.driver.get_window_size()
247280

@@ -256,6 +289,17 @@ def _capture_fullpage_screenshot_to_file(self, filename):
256289
import time
257290
time.sleep(0.5)
258291

292+
# Verify the window actually resized to requested dimensions
293+
# In non-headless mode, browsers may be limited by screen size
294+
actual_size = self.driver.get_window_size()
295+
if actual_size['height'] < full_height * 0.95: # Allow 5% tolerance for browser chrome
296+
self.warn(
297+
f"Browser window could not be resized to full page height. "
298+
f"Requested: {full_height}px, Actual: {actual_size['height']}px. "
299+
f"Screenshot may not capture the complete page. "
300+
f"Consider running in headless mode for better full-page screenshot support."
301+
)
302+
259303
# Now take the screenshot
260304
path = self._get_screenshot_path(filename)
261305
self._create_directory(path)
@@ -269,7 +313,34 @@ def _capture_fullpage_screenshot_to_file(self, filename):
269313
self.driver.set_window_size(original_size['width'], original_size['height'])
270314

271315
def _capture_fullpage_screen_to_log(self, return_val):
272-
"""Get fullpage screenshot as base64 or embed it."""
316+
"""Get fullpage screenshot as base64 or embed it using best available method."""
317+
screenshot_as_base64 = None
318+
319+
# Try CDP first (Chrome/Edge/Chromium) - works in both headless and non-headless
320+
if self._supports_cdp():
321+
screenshot_as_base64 = self._capture_fullpage_via_cdp_base64()
322+
if screenshot_as_base64:
323+
self.debug("Full-page screenshot captured using Chrome DevTools Protocol")
324+
325+
# Try Firefox native method
326+
if not screenshot_as_base64 and self._supports_native_fullpage():
327+
screenshot_as_base64 = self._capture_fullpage_via_firefox_base64()
328+
if screenshot_as_base64:
329+
self.debug("Full-page screenshot captured using Firefox native method")
330+
331+
# Fallback to resize method
332+
if not screenshot_as_base64:
333+
self.debug("Using window resize method for full-page screenshot")
334+
screenshot_as_base64 = self._capture_fullpage_via_resize_base64()
335+
336+
# Embed to log
337+
base64_str = self._embed_to_log_as_base64(screenshot_as_base64, 800)
338+
if return_val == BASE64:
339+
return base64_str
340+
return EMBED
341+
342+
def _capture_fullpage_via_resize_base64(self):
343+
"""Fallback method: Get fullpage screenshot as base64 by resizing window."""
273344
# Remember current window size so we can restore it later
274345
original_size = self.driver.get_window_size()
275346

@@ -284,17 +355,127 @@ def _capture_fullpage_screen_to_log(self, return_val):
284355
import time
285356
time.sleep(0.5)
286357

358+
# Verify the window actually resized to requested dimensions
359+
# In non-headless mode, browsers may be limited by screen size
360+
actual_size = self.driver.get_window_size()
361+
if actual_size['height'] < full_height * 0.95: # Allow 5% tolerance for browser chrome
362+
self.warn(
363+
f"Browser window could not be resized to full page height. "
364+
f"Requested: {full_height}px, Actual: {actual_size['height']}px. "
365+
f"Screenshot may not capture the complete page. "
366+
f"Consider running in headless mode for better full-page screenshot support."
367+
)
368+
287369
# Take the screenshot as base64
288370
screenshot_as_base64 = self.driver.get_screenshot_as_base64()
289-
base64_str = self._embed_to_log_as_base64(screenshot_as_base64, 800)
290-
if return_val == BASE64:
291-
return base64_str
292-
return EMBED
371+
return screenshot_as_base64
293372

294373
finally:
295374
# Put the window back to its original size
296375
self.driver.set_window_size(original_size['width'], original_size['height'])
297376

377+
def _get_browser_name(self):
378+
"""Get the name of the current browser."""
379+
try:
380+
return self.driver.capabilities.get('browserName', '').lower()
381+
except:
382+
return ''
383+
384+
def _supports_cdp(self):
385+
"""Check if browser supports Chrome DevTools Protocol."""
386+
browser_name = self._get_browser_name()
387+
return browser_name in ['chrome', 'chromium', 'msedge', 'edge', 'MicrosoftEdge']
388+
389+
def _supports_native_fullpage(self):
390+
"""Check if browser supports native full-page screenshots."""
391+
browser_name = self._get_browser_name()
392+
return browser_name == 'firefox'
393+
394+
def _capture_fullpage_via_cdp(self, filename):
395+
"""Capture full-page screenshot using Chrome DevTools Protocol."""
396+
try:
397+
# Get page dimensions
398+
metrics = self.driver.execute_cdp_cmd('Page.getLayoutMetrics', {})
399+
width = int(metrics['contentSize']['width'])
400+
height = int(metrics['contentSize']['height'])
401+
402+
# Capture screenshot with full page dimensions
403+
screenshot = self.driver.execute_cdp_cmd('Page.captureScreenshot', {
404+
'clip': {
405+
'width': width,
406+
'height': height,
407+
'x': 0,
408+
'y': 0,
409+
'scale': 1
410+
},
411+
'captureBeyondViewport': True
412+
})
413+
414+
# Save the screenshot
415+
path = self._get_screenshot_path(filename)
416+
self._create_directory(path)
417+
418+
with open(path, 'wb') as f:
419+
f.write(base64.b64decode(screenshot['data']))
420+
421+
self._embed_to_log_as_file(path, 800)
422+
return path
423+
except Exception as e:
424+
self.debug(f"CDP full-page screenshot failed: {e}. Falling back to resize method.")
425+
return None
426+
427+
def _capture_fullpage_via_cdp_base64(self):
428+
"""Capture full-page screenshot using CDP and return as base64."""
429+
try:
430+
# Get page dimensions
431+
metrics = self.driver.execute_cdp_cmd('Page.getLayoutMetrics', {})
432+
width = int(metrics['contentSize']['width'])
433+
height = int(metrics['contentSize']['height'])
434+
435+
# Capture screenshot with full page dimensions
436+
screenshot = self.driver.execute_cdp_cmd('Page.captureScreenshot', {
437+
'clip': {
438+
'width': width,
439+
'height': height,
440+
'x': 0,
441+
'y': 0,
442+
'scale': 1
443+
},
444+
'captureBeyondViewport': True
445+
})
446+
447+
return screenshot['data']
448+
except Exception as e:
449+
self.debug(f"CDP full-page screenshot failed: {e}. Falling back to resize method.")
450+
return None
451+
452+
def _capture_fullpage_via_firefox(self, filename):
453+
"""Capture full-page screenshot using Firefox native method."""
454+
try:
455+
path = self._get_screenshot_path(filename)
456+
self._create_directory(path)
457+
458+
# Firefox has a native full-page screenshot method
459+
screenshot_binary = self.driver.get_full_page_screenshot_as_png()
460+
461+
with open(path, 'wb') as f:
462+
f.write(screenshot_binary)
463+
464+
self._embed_to_log_as_file(path, 800)
465+
return path
466+
except Exception as e:
467+
self.debug(f"Firefox native full-page screenshot failed: {e}. Falling back to resize method.")
468+
return None
469+
470+
def _capture_fullpage_via_firefox_base64(self):
471+
"""Capture full-page screenshot using Firefox and return as base64."""
472+
try:
473+
screenshot_binary = self.driver.get_full_page_screenshot_as_png()
474+
return base64.b64encode(screenshot_binary).decode('utf-8')
475+
except Exception as e:
476+
self.debug(f"Firefox native full-page screenshot failed: {e}. Falling back to resize method.")
477+
return None
478+
298479
@property
299480
def _screenshot_root_directory(self):
300481
return self.ctx.screenshot_root_directory
@@ -445,7 +626,7 @@ def _print_page_as_pdf_to_file(self, filename, options):
445626
return path
446627

447628
def _save_pdf_to_file(self, pdfbase64, path):
448-
pdfdata = b64decode(pdfbase64)
629+
pdfdata = base64.b64decode(pdfbase64)
449630
with open(path, mode='wb') as pdf:
450631
pdf.write(pdfdata)
451632

0 commit comments

Comments
 (0)