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
84 changes: 84 additions & 0 deletions integration_test/cases/browser/fullpage_screenshot_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
defmodule Wallaby.Integration.Browser.FullpageScreenshotTest do
use Wallaby.Integration.SessionCase, async: false

import Wallaby.SettingsTestHelpers

alias Wallaby.TestSupport.TestWorkspace

setup %{session: session} do
page =
session
|> visit("/")

{:ok, page: page}
end

test "taking fullpage screenshots", %{page: page} do
screenshots_path = TestWorkspace.generate_temporary_path()

ensure_setting_is_reset(:wallaby, :screenshot_dir)
Application.put_env(:wallaby, :screenshot_dir, screenshots_path)

[path] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[path] =
assert [path] =

page
|> take_screenshot(name: "fullpage_test", full_page: true)
|> Map.get(:screenshots)

assert_in_directory(path, screenshots_path)
assert Path.basename(path) == "fullpage_test.png"
assert_file_exists(path)

# Verify the file is a valid PNG by checking the PNG signature
{:ok, file_content} = File.read(path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just use File.read! here, so if it fails, it fails with the right error and not a match error.

<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _rest::binary>> = file_content
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _rest::binary>> = file_content
assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _rest::binary>> = file_content

end

test "fullpage screenshot option defaults to false", %{page: page} do
screenshots_path = TestWorkspace.generate_temporary_path()

ensure_setting_is_reset(:wallaby, :screenshot_dir)
Application.put_env(:wallaby, :screenshot_dir, screenshots_path)

# Both of these should work the same way (viewport screenshot)
[path1] = page |> take_screenshot(name: "test1") |> Map.get(:screenshots)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[path1] = page |> take_screenshot(name: "test1") |> Map.get(:screenshots)
assert [path1] = page |> take_screenshot(name: "test1") |> Map.get(:screenshots)

[path2] = page |> take_screenshot(name: "test2", full_page: false) |> Map.get(:screenshots)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[path2] = page |> take_screenshot(name: "test2", full_page: false) |> Map.get(:screenshots)
assert [path2] = page |> take_screenshot(name: "test2", full_page: false) |> Map.get(:screenshots)


assert_file_exists(path1)
assert_file_exists(path2)
end

test "fullpage screenshot can be combined with log option", %{page: page} do
screenshots_path = TestWorkspace.generate_temporary_path()

ensure_setting_is_reset(:wallaby, :screenshot_dir)
Application.put_env(:wallaby, :screenshot_dir, screenshots_path)

import ExUnit.CaptureIO

output =
capture_io(fn ->
page
|> take_screenshot(name: "fullpage_logged", full_page: true, log: true)
end)

assert output =~ "Screenshot taken, find it at"
assert output =~ "fullpage_logged.png"
end

defp assert_in_directory(path, directory) do
assert Path.expand(directory) == Path.expand(Path.dirname(path)), """
Path is not in expected directory.
path: #{inspect(path)}
directory: #{inspect(directory)}
"""
end

defp assert_file_exists(path) do
assert path |> Path.expand() |> File.exists?(), """
File does not exist
path: #{inspect(path)}
"""
end
end
22 changes: 19 additions & 3 deletions lib/wallaby/browser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,30 @@ defmodule Wallaby.Browser do

Pass `[{:name, "some_name"}]` to specify the file name. Defaults to a timestamp.
Pass `[{:log, true}]` to log the location of the screenshot to stdout. Defaults to false.
Pass `[{:full_page, true}]` to capture the entire page, not just the viewport. Defaults to false.

## Full Page Screenshots

When `full_page: true` is specified:
- Chrome: Uses Chrome DevTools Protocol (CDP) for native fullpage capture
- Firefox: Uses GeckoDriver's Moz-specific fullpage screenshot endpoint
Comment on lines +235 to +236
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this information relevant? I feel like implies to the user that they should know how these things work, but the respective webdriver takes care of it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to indicate which versions of the webdriver that supports this functionality


Full page screenshots capture the entire document, including content outside the viewport.
This is useful for capturing long pages without scrolling or stitching multiple screenshots.
Both implementations use native browser APIs for accurate rendering.
"""
@type take_screenshot_opt :: {:name, String.t()} | {:log, boolean}
@type take_screenshot_opt :: {:name, String.t()} | {:log, boolean} | {:full_page, boolean}
@spec take_screenshot(parent, [take_screenshot_opt]) :: parent

def take_screenshot(%{driver: driver} = screenshotable, opts \\ []) do
image_data =
screenshotable
|> driver.take_screenshot
if opts[:full_page] do
screenshotable
|> driver.take_fullpage_screenshot()
else
screenshotable
|> driver.take_screenshot()
end

name =
opts
Expand Down
7 changes: 7 additions & 0 deletions lib/wallaby/chrome.ex
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,13 @@ defmodule Wallaby.Chrome do
def element_location(element), do: delegate(:element_location, element)
@doc false
def take_screenshot(session_or_element), do: delegate(:take_screenshot, session_or_element)
@doc false
def take_fullpage_screenshot(session_or_element) do
check_logs!(session_or_element, fn ->
WebdriverClient.take_fullpage_screenshot_cdp(session_or_element)
end)
end

@doc false
defdelegate log(session_or_element), to: WebdriverClient

Expand Down
6 changes: 6 additions & 0 deletions lib/wallaby/driver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ defmodule Wallaby.Driver do
"""
@callback take_screenshot(Session.t() | Element.t()) :: binary | {:error, reason}

@doc """
Invoked to take a fullpage screenshot of the session.
This uses browser-specific APIs to capture the entire page, not just the viewport.
"""
@callback take_fullpage_screenshot(Session.t() | Element.t()) :: binary | {:error, reason}

@doc """
Invoked to get the handle for the currently focused window.
"""
Expand Down
5 changes: 5 additions & 0 deletions lib/wallaby/selenium.ex
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ defmodule Wallaby.Selenium do
@doc false
defdelegate take_screenshot(session_or_element), to: WebdriverClient

@doc false
def take_fullpage_screenshot(session_or_element) do
WebdriverClient.take_fullpage_screenshot_moz(session_or_element)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, selenium is not only used for Firefox. You can use it with any browser/web driver it supports. I think this code won't work if you use selenium with chrome.

end

@doc false
def cookies(%Session{} = session) do
WebdriverClient.cookies(session)
Expand Down
45 changes: 45 additions & 0 deletions lib/wallaby/webdriver_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,51 @@ defmodule Wallaby.WebdriverClient do
end
end

@doc """
Executes a Chrome DevTools Protocol (CDP) command.
Only works with ChromeDriver.
"""
@spec execute_cdp(Session.t(), String.t(), map) :: {:ok, any} | {:error, any}
def execute_cdp(session, command, params \\ %{}) do
request_params = %{
cmd: command,
params: params
}

with {:ok, resp} <- request(:post, "#{session.session_url}/goog/cdp/execute", request_params) do
Map.fetch(resp, "value")
end
end

@doc """
Takes a fullpage screenshot using Chrome DevTools Protocol.
Only works with ChromeDriver.
"""
@spec take_fullpage_screenshot_cdp(Session.t()) :: binary | {:error, any}
def take_fullpage_screenshot_cdp(session) do
params = %{
format: "png",
captureBeyondViewport: true
}

with {:ok, result} <- execute_cdp(session, "Page.captureScreenshot", params),
{:ok, data} <- Map.fetch(result, "data") do
:base64.decode(data)
end
end

@doc """
Takes a fullpage screenshot using Firefox's Moz-specific extension.
Only works with GeckoDriver/Firefox.
"""
@spec take_fullpage_screenshot_moz(Session.t()) :: binary | {:error, any}
def take_fullpage_screenshot_moz(session) do
with {:ok, resp} <- request(:get, "#{session.session_url}/moz/screenshot/full"),
{:ok, value} <- Map.fetch(resp, "value") do
:base64.decode(value)
end
end

@doc """
Gets the cookies for a session.
"""
Expand Down
Loading