diff --git a/integration_test/cases/browser/fullpage_screenshot_test.exs b/integration_test/cases/browser/fullpage_screenshot_test.exs new file mode 100644 index 00000000..33b2a336 --- /dev/null +++ b/integration_test/cases/browser/fullpage_screenshot_test.exs @@ -0,0 +1,59 @@ +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("/long_page.html") + + screenshots_path = TestWorkspace.generate_temporary_path() + ensure_setting_is_reset(:wallaby, :screenshot_dir) + Application.put_env(:wallaby, :screenshot_dir, screenshots_path) + + {:ok, page: page, screenshots_path: screenshots_path} + end + + test "fullpage screenshot captures content beyond the viewport", %{page: page} do + assert [viewport_path] = + page + |> take_screenshot(name: "viewport") + |> Map.get(:screenshots) + + assert [fullpage_path] = + page + |> take_screenshot(name: "fullpage", full_page: true) + |> Map.get(:screenshots) + + viewport_size = File.stat!(viewport_path).size + fullpage_size = File.stat!(fullpage_path).size + + assert fullpage_size > viewport_size + end + + test "full_page option defaults to false", %{page: page, screenshots_path: screenshots_path} do + assert [path] = + page + |> take_screenshot(name: "default") + |> Map.get(:screenshots) + + assert path |> Path.expand() |> File.exists?() + assert Path.dirname(Path.expand(path)) == Path.expand(screenshots_path) + end + + test "full_page can be combined with other options", %{page: page} do + 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 +end diff --git a/integration_test/support/pages/long_page.html b/integration_test/support/pages/long_page.html new file mode 100644 index 00000000..852be3e4 --- /dev/null +++ b/integration_test/support/pages/long_page.html @@ -0,0 +1,13 @@ + + + + Long Page + + +

Long Page

+
+

This content extends well beyond the viewport.

+
+

Bottom of the page

+ + diff --git a/lib/wallaby/browser.ex b/lib/wallaby/browser.ex index 63ef77b6..48f1dd3c 100644 --- a/lib/wallaby/browser.ex +++ b/lib/wallaby/browser.ex @@ -227,14 +227,27 @@ 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, the entire document is captured including content + outside the viewport. Supported drivers: + - ChromeDriver (Chrome) + - GeckoDriver 0.16.0+ (Firefox via Selenium) """ - @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 diff --git a/lib/wallaby/chrome.ex b/lib/wallaby/chrome.ex index cb693d16..c9e04fb9 100644 --- a/lib/wallaby/chrome.ex +++ b/lib/wallaby/chrome.ex @@ -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(session_or_element) + end) + end + @doc false defdelegate log(session_or_element), to: WebdriverClient diff --git a/lib/wallaby/driver.ex b/lib/wallaby/driver.ex index 79630fc3..3b154a2f 100644 --- a/lib/wallaby/driver.ex +++ b/lib/wallaby/driver.ex @@ -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()) :: binary | {:error, reason} + @doc """ Invoked to get the handle for the currently focused window. """ diff --git a/lib/wallaby/selenium.ex b/lib/wallaby/selenium.ex index 803dcf5f..709be606 100644 --- a/lib/wallaby/selenium.ex +++ b/lib/wallaby/selenium.ex @@ -197,6 +197,9 @@ defmodule Wallaby.Selenium do @doc false defdelegate take_screenshot(session_or_element), to: WebdriverClient + @doc false + defdelegate take_fullpage_screenshot(session_or_element), to: WebdriverClient + @doc false def cookies(%Session{} = session) do WebdriverClient.cookies(session) diff --git a/lib/wallaby/webdriver_client.ex b/lib/wallaby/webdriver_client.ex index 92adf7d6..a633cba4 100644 --- a/lib/wallaby/webdriver_client.ex +++ b/lib/wallaby/webdriver_client.ex @@ -403,6 +403,43 @@ defmodule Wallaby.WebdriverClient do end end + defp 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 of the entire document. + Uses the Moz-specific endpoint for Firefox, and Chrome DevTools Protocol for all other browsers. + """ + @spec take_fullpage_screenshot(Session.t()) :: binary | {:error, any} + # Firefox: uses GeckoDriver's Moz-specific endpoint + def take_fullpage_screenshot(%Session{capabilities: %{browserName: "firefox"}} = 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 + + # Chrome and other Chromium-based browsers: uses Chrome DevTools Protocol + def take_fullpage_screenshot(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 """ Gets the cookies for a session. """