Skip to content

Commit

Permalink
#1799 implement full-size screenshots as a separate Selenide plugin
Browse files Browse the repository at this point in the history
this is a re-worked PR #1800 and PR #1831
  • Loading branch information
asolntsev committed Jun 25, 2022
1 parent ed51dbf commit 251349f
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 0 deletions.
14 changes: 14 additions & 0 deletions modules/full-screenshot/build.gradle
@@ -0,0 +1,14 @@
ext {
artifactId = 'selenide-full-screenshot'
}

dependencies {
api project(":statics")
testImplementation project(':statics').sourceSets.test.output
testImplementation project(':modules:core').sourceSets.test.output
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.junit.platform:junit-platform-suite-engine:1.8.2")
}

apply from: rootProject.file('gradle/publish-module.gradle')
@@ -0,0 +1,112 @@
package com.codeborne.selenide.fullscreenshot;

import com.codeborne.selenide.Driver;
import com.codeborne.selenide.impl.JavaScript;
import com.codeborne.selenide.impl.Photographer;
import com.codeborne.selenide.impl.WebdriverPhotographer;
import com.github.bsideup.jabel.Desugar;
import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WrapsDriver;
import org.openqa.selenium.chromium.HasCdp;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.HasDevTools;
import org.openqa.selenium.devtools.v101.page.Page;
import org.openqa.selenium.devtools.v101.page.model.Viewport;
import org.openqa.selenium.firefox.HasFullPageScreenshot;
import org.openqa.selenium.remote.Augmenter;
import org.openqa.selenium.remote.RemoteWebDriver;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Map;
import java.util.Optional;

/**
* Implementation of {@link Photographer} which can take full-size screenshots.
* @since 6.7.0
*/
@ParametersAreNonnullByDefault
public class FullSizePhotographer implements Photographer {
private static final JavaScript js = new JavaScript("get-screen-size.js");

private final WebdriverPhotographer defaultImplementation;

public FullSizePhotographer() {
this(new WebdriverPhotographer());
}

protected FullSizePhotographer(WebdriverPhotographer defaultImplementation) {
this.defaultImplementation = defaultImplementation;
}

@Nonnull
@Override
@CheckReturnValue
public <T> Optional<T> takeScreenshot(Driver driver, OutputType<T> outputType) {
WebDriver wd = driver.getWebDriver();
if (wd instanceof WrapsDriver) {
wd = ((WrapsDriver) wd).getWrappedDriver();
}
if (wd instanceof HasFullPageScreenshot webDriver) {
return Optional.of(webDriver.getFullPageScreenshotAs(outputType));
}
if (wd instanceof HasCdp cdp) {
Options options = getOptions(wd);
Map<String, Object> captureScreenshotOptions = ImmutableMap.of(
"clip", ImmutableMap.of(
"x", 0,
"y", 0,
"width", options.fullWidth(),
"height", options.fullHeight(),
"scale", 1),
"captureBeyondViewport", options.exceedViewport()
);

Map<String, Object> result = cdp.executeCdpCommand("Page.captureScreenshot", captureScreenshotOptions);

String base64 = (String) result.get("data");
T screenshot = outputType.convertFromBase64Png(base64);
return Optional.of(screenshot);
}

if (wd instanceof RemoteWebDriver remoteWebDriver) {
WebDriver webDriver = new Augmenter().augment(remoteWebDriver);
if (webDriver instanceof HasFullPageScreenshot smartWebDriver) {
return Optional.of(smartWebDriver.getFullPageScreenshotAs(outputType));
}
if (!(webDriver instanceof HasDevTools)) {
return defaultImplementation.takeScreenshot(driver, outputType);
}
DevTools devTools = ((HasDevTools) webDriver).getDevTools();
devTools.createSession();

Options options = getOptions(remoteWebDriver);
Viewport viewport = new Viewport(0, 0, options.fullWidth(), options.fullHeight(), 1);

String base64 = devTools.send(Page.captureScreenshot(
Optional.empty(),
Optional.empty(),
Optional.of(viewport),
Optional.empty(),
Optional.of(options.exceedViewport())
)
);

T screenshot = outputType.convertFromBase64Png(base64);
return Optional.of(screenshot);
}
return Optional.empty();
}

private Options getOptions(WebDriver webDriver) {
Map<String, Object> size = js.execute(webDriver);
return new Options((long) size.get("fullWidth"), (long) size.get("fullHeight"), (boolean) size.get("exceedViewport"));
}

@Desugar
private record Options(long fullWidth, long fullHeight, boolean exceedViewport) {
}
}
@@ -0,0 +1,8 @@
/**
* Selenide plugin for taking full-size screenshots.
* <p>
* These screenshots cover the whole browser window which might be bigger than the area currently visible on the screen.
* </p>
* @since 6.7.0
*/
package com.codeborne.selenide.fullscreenshot;
@@ -0,0 +1 @@
com.codeborne.selenide.fullscreenshot.FullSizePhotographer
8 changes: 8 additions & 0 deletions modules/full-screenshot/src/main/resources/get-screen-size.js
@@ -0,0 +1,8 @@
(() => {
const fullWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.body.clientWidth, document.documentElement.clientWidth);
const fullHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight);
const viewWidth = window.innerWidth;
const viewHeight = window.innerHeight;
const exceedViewport = fullWidth > viewWidth || fullHeight > viewHeight;
return {fullWidth, fullHeight, exceedViewport};
})()
@@ -0,0 +1,13 @@
package integration;

import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;

@Suite
@SelectClasses({
ScreenshotsTest.class,
ScreenshotTest.class,
ScreenshotInIframeTest.class
})
public class ExistingScreenshotTestsWithFullSizePhotographer {
}
@@ -0,0 +1,32 @@
package integration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

public class ScreenshotTestHelper {
private static final Logger log = LoggerFactory.getLogger(ScreenshotTestHelper.class);

static void verifyScreenshotSize(File screenshot, int width, int height) throws IOException {
BufferedImage img = ImageIO.read(screenshot);
log.info("Verify screenshot {} of size {}x{}", screenshot.getAbsolutePath(), img.getWidth(), img.getHeight());
if (nearlyEqual(img.getWidth(), width * 2) && nearlyEqual(img.getHeight(), height * 2)) {
// it's Retina display, it has 2x more pixels
log.info("Screenshot matches {}x{} on Retina display", width, height);
}
else {
assertThat(img.getWidth()).isBetween(width - 50, width + 50);
assertThat(img.getHeight()).isBetween(height - 50, height + 50);
}
}

private static boolean nearlyEqual(int actual, int expected) {
return actual > expected - 50 && actual < expected + 50;
}
}
@@ -0,0 +1,59 @@
package integration;

import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.WebDriverRunner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.support.events.AbstractWebDriverEventListener;
import org.openqa.selenium.support.events.WebDriverListener;

import java.io.File;
import java.io.IOException;

import static integration.ScreenshotTestHelper.verifyScreenshotSize;

@SuppressWarnings("deprecation")
public class ScreenshotsWithWebdriverListenersTest extends IntegrationTest {
private final DeprecatedListener deprecatedListener = new DeprecatedListener();
private final Selenium4Listener listener = new Selenium4Listener();
private final int width = 2200;
private final int height = 3300;

@AfterEach
@BeforeEach
void tearDown() {
WebDriverRunner.removeListener(listener);
WebDriverRunner.removeListener(deprecatedListener);
Selenide.closeWebDriver();
Configuration.browserSize = "400x300";
}

@Test
void canTakeFullScreenshotWithSelenium4Listeners() throws IOException {
WebDriverRunner.addListener(listener);
openFile("page_of_fixed_size_2200x3300.html");

File screenshot = Selenide.screenshot(OutputType.FILE);

verifyScreenshotSize(screenshot, width, height);
}

@Test
void canTakeFullScreenshotWithSelenium3Listeners() throws IOException {
WebDriverRunner.addListener(deprecatedListener);
openFile("page_of_fixed_size_2200x3300.html");

File screenshot = Selenide.screenshot(OutputType.FILE);

verifyScreenshotSize(screenshot, width, height);
}

public static class Selenium4Listener implements WebDriverListener {
}

public static class DeprecatedListener extends AbstractWebDriverEventListener {
}
}
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test::page of fixed size 2200x3300</title>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
#the_div {
text-align: center;
border: red 3px solid;
width: 2200px;
height: 3300px;
background: #0ff;
}
</style>
</head>
<body>
<div id="the_div">
This should be a div of size 2222x3333
</div>
</body>
</html>
1 change: 1 addition & 0 deletions settings.gradle
Expand Up @@ -6,3 +6,4 @@ include ':modules:grid'
include ':modules:junit4'
include ':modules:testng'
include ':modules:clear-with-shortcut'
include ':modules:full-screenshot'

0 comments on commit 251349f

Please sign in to comment.