diff --git a/atest/acceptance/keywords/screenshot_fullpage.robot b/atest/acceptance/keywords/screenshot_fullpage.robot
new file mode 100644
index 000000000..0b5c88af9
--- /dev/null
+++ b/atest/acceptance/keywords/screenshot_fullpage.robot
@@ -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
+ [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}
diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py
index 11308af85..a2ed7b882 100644
--- a/src/SeleniumLibrary/keywords/screenshot.py
+++ b/src/SeleniumLibrary/keywords/screenshot.py
@@ -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
@@ -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]
@@ -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
@@ -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
@@ -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)
diff --git a/utest/test/keywords/test_screen_shot.py b/utest/test/keywords/test_screen_shot.py
index 2ea09cb30..324e08622 100644
--- a/utest/test/keywords/test_screen_shot.py
+++ b/utest/test/keywords/test_screen_shot.py
@@ -7,6 +7,7 @@
SCREENSHOT_FILE_NAME = "selenium-screenshot-{index}.png"
ELEMENT_FILE_NAME = "selenium-element-screenshot-{index}.png"
+FULLPAGE_FILE_NAME = "selenium-fullpage-screenshot-{index}.png"
EMBED = "EMBED"
BASE64 = "BASE64"
@@ -102,3 +103,26 @@ def test_sl_set_screenshot_directory():
cur_dir = dirname(abspath(__file__))
sl.set_screenshot_directory(cur_dir)
assert sl.screenshot_root_directory == cur_dir
+
+
+def test_fullpage_defaults(screen_shot):
+ assert screen_shot._decide_embedded(FULLPAGE_FILE_NAME) == (False, None)
+
+
+def test_fullpage_screen_shotdir_embeded(screen_shot):
+ screen_shot.ctx.screenshot_root_directory = EMBED
+ assert screen_shot._decide_embedded(FULLPAGE_FILE_NAME) == (True, EMBED)
+ assert screen_shot._decide_embedded(FULLPAGE_FILE_NAME.upper()) == (True, EMBED)
+
+
+def test_fullpage_screen_shotdir_return_base64(screen_shot):
+ screen_shot.ctx.screenshot_root_directory = BASE64
+ assert screen_shot._decide_embedded(FULLPAGE_FILE_NAME) == (True, BASE64)
+ assert screen_shot._decide_embedded(FULLPAGE_FILE_NAME.upper()) == (True, BASE64)
+
+
+def test_fullpage_file_name_embeded(screen_shot):
+ screen_shot.ctx.screenshot_root_directory = EMBED
+ assert screen_shot._decide_embedded(EMBED) == (True, EMBED)
+ screen_shot.ctx.screenshot_root_directory = BASE64
+ assert screen_shot._decide_embedded(BASE64) == (True, BASE64)
|