Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions atest/acceptance/keywords/screenshot_fullpage.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
*** Settings ***
Documentation Tests fullpage screenshots
Suite Setup Go To Page "forms.html"
Resource ../resource.robot
Force Tags Known Issue Internet Explorer

*** Test Cases ***
Capture fullpage screenshot to default location
[Tags] NoGrid
[Documentation]
... LOG 1:5 </td></tr><tr><td colspan="3"><a href="selenium-fullpage-screenshot-1.png"><img src="selenium-fullpage-screenshot-1.png" width="800px"></a>
... LOG 7:5 </td></tr><tr><td colspan="3"><a href="selenium-fullpage-screenshot-2.png"><img src="selenium-fullpage-screenshot-2.png" width="800px"></a>
[Setup] Remove Files ${OUTPUTDIR}/selenium-fullpage-screenshot-*.png
${file} = Capture Fullpage Screenshot
${count} = Count Files In Directory ${OUTPUTDIR} selenium-fullpage-screenshot-*.png
Should Be Equal As Integers ${count} 1
Should Be Equal ${file} ${OUTPUTDIR}${/}selenium-fullpage-screenshot-1.png
Click Link Relative
Wait Until Page Contains Element tag=body
Capture Fullpage Screenshot
${count} = Count Files In Directory ${OUTPUTDIR} selenium-fullpage-screenshot-*.png
Should Be Equal As Integers ${count} 2

Capture fullpage screenshot to custom file
[Setup] Remove Files ${OUTPUTDIR}/custom-fullpage-screenshot.png
Capture Fullpage Screenshot custom-fullpage-screenshot.png
File Should Exist ${OUTPUTDIR}/custom-fullpage-screenshot.png

Capture fullpage screenshot to custom directory
[Setup] Remove Files ${TEMPDIR}/seleniumlibrary-fullpage-screenshot-test.png
Create Directory ${TEMPDIR}
Set Screenshot Directory ${TEMPDIR}
Capture Fullpage Screenshot seleniumlibrary-fullpage-screenshot-test.png
File Should Exist ${TEMPDIR}/seleniumlibrary-fullpage-screenshot-test.png

Capture fullpage screenshot with index
[Setup] Remove Files ${OUTPUTDIR}/fullpage-screenshot-*.png
Capture Fullpage Screenshot fullpage-screenshot-{index}.png
Capture Fullpage Screenshot fullpage-screenshot-{index}.png
File Should Exist ${OUTPUTDIR}/fullpage-screenshot-1.png
File Should Exist ${OUTPUTDIR}/fullpage-screenshot-2.png

Capture fullpage screenshot with formatted index
[Setup] Remove Files ${OUTPUTDIR}/fullpage-screenshot-*.png
Capture Fullpage Screenshot fullpage-screenshot-{index:03}.png
File Should Exist ${OUTPUTDIR}/fullpage-screenshot-001.png

Capture fullpage screenshot embedded
[Setup] Set Screenshot Directory EMBED
${result} = Capture Fullpage Screenshot
Should Be Equal ${result} EMBED

Capture fullpage screenshot base64
[Setup] Set Screenshot Directory BASE64
${result} = Capture Fullpage Screenshot
Should Not Be Empty ${result}
Should Match Regexp ${result} ^[A-Za-z0-9+/=]+$

Capture fullpage screenshot with EMBED filename
[Setup] Set Screenshot Directory EMBED
${result} = Capture Fullpage Screenshot EMBED
Should Be Equal ${result} EMBED

Capture fullpage screenshot with BASE64 filename
[Setup] Set Screenshot Directory EMBED
${result} = Capture Fullpage Screenshot BASE64
Should Not Be Empty ${result}
Should Match Regexp ${result} ^[A-Za-z0-9+/=]+$

Capture fullpage screenshot when no browser
[Setup] Close All Browsers
${result} = Capture Fullpage Screenshot
Should Be Equal ${result} ${None}
286 changes: 284 additions & 2 deletions src/SeleniumLibrary/keywords/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import base64
from typing import Optional, Union
from base64 import b64decode

from robot.utils import get_link_path
from selenium.webdriver.remote.webelement import WebElement
Expand All @@ -26,6 +26,7 @@

DEFAULT_FILENAME_PAGE = "selenium-screenshot-{index}.png"
DEFAULT_FILENAME_ELEMENT = "selenium-element-screenshot-{index}.png"
DEFAULT_FILENAME_FULLPAGE = "selenium-fullpage-screenshot-{index}.png"
EMBED = "EMBED"
BASE64 = "BASE64"
EMBEDDED_OPTIONS = [EMBED, BASE64]
Expand Down Expand Up @@ -199,6 +200,282 @@ def _capture_element_screen_to_log(self, element, return_val):
return base64_str
return EMBED

@keyword
def capture_fullpage_screenshot(self, filename: str = DEFAULT_FILENAME_FULLPAGE) -> str:
"""Takes a screenshot of the entire page, including parts not visible in viewport.

This keyword captures the full height and width of a web page, even if it extends
beyond the current viewport. The implementation automatically selects the best
available method based on the browser:

*Screenshot Methods (in order of preference):*

1. **Chrome DevTools Protocol (CDP)**: Used for Chrome, Edge, and Chromium browsers.
Works in both headless and non-headless mode without screen size limitations.

2. **Firefox Native Method**: Used for Firefox browsers. Captures full page
using the browser's built-in capability.

3. **Window Resize Method**: Fallback for other browsers. Temporarily resizes
the browser window to match page dimensions. In non-headless mode, this may
be limited by physical screen size, and a warning will be logged if the full
page cannot be captured.

``filename`` argument specifies where to save the screenshot file.
The directory can be set with `Set Screenshot Directory` keyword or
when importing the library. If not configured, screenshots go to the
same directory as Robot Framework's log file.

If ``filename`` is EMBED (case insensitive), the screenshot gets embedded
as Base64 image in log.html without creating a file. If it's BASE64,
the base64 string is returned and also embedded in the log.

The ``{index}`` marker in filename gets replaced with a unique number
to prevent overwriting files. You can customize the format like
``{index:03}`` for zero-padded numbers.

Returns the absolute path to the screenshot file, or EMBED/BASE64 string
if those options are used.

Examples:
| `Capture Fullpage Screenshot` | |
| `File Should Exist` | ${OUTPUTDIR}/selenium-fullpage-screenshot-1.png |
| ${path} = | `Capture Fullpage Screenshot` |
| `Capture Fullpage Screenshot` | custom_fullpage.png |
| `Capture Fullpage Screenshot` | custom_{index}.png |
| `Capture Fullpage Screenshot` | EMBED |
"""
if not self.drivers.current:
self.info("Cannot capture fullpage screenshot because no browser is open.")
return
is_embedded, method = self._decide_embedded(filename)
if is_embedded:
return self._capture_fullpage_screen_to_log(method)
return self._capture_fullpage_screenshot_to_file(filename)

def _capture_fullpage_screenshot_to_file(self, filename):
"""Save fullpage screenshot to file using best available method."""
# Try CDP first (Chrome/Edge/Chromium) - works in both headless and non-headless
if self._supports_cdp():
result = self._capture_fullpage_via_cdp(filename)
if result:
self.debug("Full-page screenshot captured using Chrome DevTools Protocol")
return result

# Try Firefox native method
if self._supports_native_fullpage():
result = self._capture_fullpage_via_firefox(filename)
if result:
self.debug("Full-page screenshot captured using Firefox native method")
return result

# Fallback to resize method (works in headless mode for all browsers)
self.debug("Using window resize method for full-page screenshot")
return self._capture_fullpage_via_resize(filename)

def _capture_fullpage_via_resize(self, filename):
"""Fallback method: Save fullpage screenshot by resizing window."""
# Remember current window size so we can restore it later
original_size = self.driver.get_window_size()

try:
# Get the actual page height - this covers all the content
full_height = self.driver.execute_script("return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);")

# Resize window to show the full page
self.driver.set_window_size(original_size['width'], full_height)

# Give the page a moment to render after resize
import time
time.sleep(0.5)

# Verify the window actually resized to requested dimensions
# In non-headless mode, browsers may be limited by screen size
actual_size = self.driver.get_window_size()
if actual_size['height'] < full_height * 0.95: # Allow 5% tolerance for browser chrome
self.warn(
f"Browser window could not be resized to full page height. "
f"Requested: {full_height}px, Actual: {actual_size['height']}px. "
f"Screenshot may not capture the complete page. "
f"Consider running in headless mode for better full-page screenshot support."
)

# Now take the screenshot
path = self._get_screenshot_path(filename)
self._create_directory(path)
if not self.driver.save_screenshot(path):
raise RuntimeError(f"Failed to save fullpage screenshot '{path}'.")
self._embed_to_log_as_file(path, 800)
return path

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

def _capture_fullpage_screen_to_log(self, return_val):
"""Get fullpage screenshot as base64 or embed it using best available method."""
screenshot_as_base64 = None

# Try CDP first (Chrome/Edge/Chromium) - works in both headless and non-headless
if self._supports_cdp():
screenshot_as_base64 = self._capture_fullpage_via_cdp_base64()
if screenshot_as_base64:
self.debug("Full-page screenshot captured using Chrome DevTools Protocol")

# Try Firefox native method
if not screenshot_as_base64 and self._supports_native_fullpage():
screenshot_as_base64 = self._capture_fullpage_via_firefox_base64()
if screenshot_as_base64:
self.debug("Full-page screenshot captured using Firefox native method")

# Fallback to resize method
if not screenshot_as_base64:
self.debug("Using window resize method for full-page screenshot")
screenshot_as_base64 = self._capture_fullpage_via_resize_base64()

# Embed to log
base64_str = self._embed_to_log_as_base64(screenshot_as_base64, 800)
if return_val == BASE64:
return base64_str
return EMBED

def _capture_fullpage_via_resize_base64(self):
"""Fallback method: Get fullpage screenshot as base64 by resizing window."""
# Remember current window size so we can restore it later
original_size = self.driver.get_window_size()

try:
# Get the actual page height - this covers all the content
full_height = self.driver.execute_script("return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);")

# Resize window to show the full page
self.driver.set_window_size(original_size['width'], full_height)

# Give the page a moment to render after resize
import time
time.sleep(0.5)

# Verify the window actually resized to requested dimensions
# In non-headless mode, browsers may be limited by screen size
actual_size = self.driver.get_window_size()
if actual_size['height'] < full_height * 0.95: # Allow 5% tolerance for browser chrome
self.warn(
f"Browser window could not be resized to full page height. "
f"Requested: {full_height}px, Actual: {actual_size['height']}px. "
f"Screenshot may not capture the complete page. "
f"Consider running in headless mode for better full-page screenshot support."
)

# Take the screenshot as base64
screenshot_as_base64 = self.driver.get_screenshot_as_base64()
return screenshot_as_base64

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

def _get_browser_name(self):
"""Get the name of the current browser."""
try:
return self.driver.capabilities.get('browserName', '').lower()
except:
return ''

def _supports_cdp(self):
"""Check if browser supports Chrome DevTools Protocol."""
browser_name = self._get_browser_name()
return browser_name in ['chrome', 'chromium', 'msedge', 'edge', 'MicrosoftEdge']

def _supports_native_fullpage(self):
"""Check if browser supports native full-page screenshots."""
browser_name = self._get_browser_name()
return browser_name == 'firefox'

def _capture_fullpage_via_cdp(self, filename):
"""Capture full-page screenshot using Chrome DevTools Protocol."""
try:
# Get page dimensions
metrics = self.driver.execute_cdp_cmd('Page.getLayoutMetrics', {})
width = int(metrics['contentSize']['width'])
height = int(metrics['contentSize']['height'])

# Capture screenshot with full page dimensions
screenshot = self.driver.execute_cdp_cmd('Page.captureScreenshot', {
'clip': {
'width': width,
'height': height,
'x': 0,
'y': 0,
'scale': 1
},
'captureBeyondViewport': True
})

# Save the screenshot
path = self._get_screenshot_path(filename)
self._create_directory(path)

with open(path, 'wb') as f:
f.write(base64.b64decode(screenshot['data']))

self._embed_to_log_as_file(path, 800)
return path
except Exception as e:
self.debug(f"CDP full-page screenshot failed: {e}. Falling back to resize method.")
return None

def _capture_fullpage_via_cdp_base64(self):
"""Capture full-page screenshot using CDP and return as base64."""
try:
# Get page dimensions
metrics = self.driver.execute_cdp_cmd('Page.getLayoutMetrics', {})
width = int(metrics['contentSize']['width'])
height = int(metrics['contentSize']['height'])

# Capture screenshot with full page dimensions
screenshot = self.driver.execute_cdp_cmd('Page.captureScreenshot', {
'clip': {
'width': width,
'height': height,
'x': 0,
'y': 0,
'scale': 1
},
'captureBeyondViewport': True
})

return screenshot['data']
except Exception as e:
self.debug(f"CDP full-page screenshot failed: {e}. Falling back to resize method.")
return None

def _capture_fullpage_via_firefox(self, filename):
"""Capture full-page screenshot using Firefox native method."""
try:
path = self._get_screenshot_path(filename)
self._create_directory(path)

# Firefox has a native full-page screenshot method
screenshot_binary = self.driver.get_full_page_screenshot_as_png()

with open(path, 'wb') as f:
f.write(screenshot_binary)

self._embed_to_log_as_file(path, 800)
return path
except Exception as e:
self.debug(f"Firefox native full-page screenshot failed: {e}. Falling back to resize method.")
return None

def _capture_fullpage_via_firefox_base64(self):
"""Capture full-page screenshot using Firefox and return as base64."""
try:
screenshot_binary = self.driver.get_full_page_screenshot_as_png()
return base64.b64encode(screenshot_binary).decode('utf-8')
except Exception as e:
self.debug(f"Firefox native full-page screenshot failed: {e}. Falling back to resize method.")
return None

@property
def _screenshot_root_directory(self):
return self.ctx.screenshot_root_directory
Expand All @@ -219,6 +496,11 @@ def _decide_embedded(self, filename):
and self._screenshot_root_directory in EMBEDDED_OPTIONS
):
return True, self._screenshot_root_directory
if (
filename == DEFAULT_FILENAME_FULLPAGE.upper()
and self._screenshot_root_directory in EMBEDDED_OPTIONS
):
return True, self._screenshot_root_directory
if filename in EMBEDDED_OPTIONS:
return True, self._screenshot_root_directory
return False, None
Expand Down Expand Up @@ -344,7 +626,7 @@ def _print_page_as_pdf_to_file(self, filename, options):
return path

def _save_pdf_to_file(self, pdfbase64, path):
pdfdata = b64decode(pdfbase64)
pdfdata = base64.b64decode(pdfbase64)
with open(path, mode='wb') as pdf:
pdf.write(pdfdata)

Expand Down
Loading