Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1799 implement full-size screenshots as a separate Selenide plugin #1858

Merged
merged 3 commits into from Aug 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,147 @@
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.WebDriverException;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 Logger log = LoggerFactory.getLogger(FullSizePhotographer.class);
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) {
try {
Optional<T> result = takeFullSizeScreenshot(driver, outputType);
return result.isPresent() ? result :
defaultImplementation.takeScreenshot(driver, outputType);
}
catch (WebDriverException e) {
log.error("Failed to take full-size screenshot", e);
return defaultImplementation.takeScreenshot(driver, outputType);
}
}

@Nonnull
private <T> Optional<T> takeFullSizeScreenshot(Driver driver, OutputType<T> outputType) {
WebDriver webDriver = unwrap(driver);

if (webDriver instanceof HasFullPageScreenshot firefoxDriver) {
return Optional.of(firefoxDriver.getFullPageScreenshotAs(outputType));
}
if (webDriver instanceof HasCdp) {
return takeScreenshotWithCDP((WebDriver & HasCdp) webDriver, outputType);
}
if (webDriver instanceof HasDevTools) {
return takeScreenshot((WebDriver & HasDevTools) webDriver, outputType);
}
return Optional.empty();
}

private WebDriver unwrap(Driver driver) {
WebDriver webDriver = driver.getWebDriver();
if (webDriver instanceof WrapsDriver) {
webDriver = ((WrapsDriver) webDriver).getWrappedDriver();
}
if (webDriver instanceof RemoteWebDriver remoteWebDriver) {
webDriver = new Augmenter().augment(remoteWebDriver);
}
return webDriver;
}

@Nonnull
@CheckReturnValue
private <WD extends WebDriver & HasDevTools, ResultType> Optional<ResultType> takeScreenshot(
WD devtoolsDriver, OutputType<ResultType> outputType
) {
DevTools devTools = devtoolsDriver.getDevTools();
devTools.createSessionIfThereIsNotOne();

Options options = getOptions(devtoolsDriver);
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())
)
);

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

@Nonnull
@CheckReturnValue
private <WD extends WebDriver & HasCdp, ResultType> Optional<ResultType> takeScreenshotWithCDP(
WD cdpDriver, OutputType<ResultType> outputType
) {
Options options = getOptions(cdpDriver);
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 = cdpDriver.executeCdpCommand("Page.captureScreenshot", captureScreenshotOptions);

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

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,41 @@
package integration;

import com.codeborne.selenide.Configuration;
import org.apache.commons.io.FileUtils;
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 java.util.UUID;

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

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{} size on Retina display", width, height);
}
else if (nearlyEqual(img.getWidth(), width) && nearlyEqual(img.getHeight(), height)) {
log.info("Screenshot matches {}x{} size", width, height);
}
else {
File archivedFile = new File(Configuration.reportsFolder, UUID.randomUUID() + ".png");
FileUtils.copyFile(screenshot, archivedFile);
log.info("Screenshot matches {}x{} size", width, height);
fail(String.format("Screenshot %s is expected to have size %sx%s, but actual size: %sx%s",
archivedFile.getAbsolutePath(), width, height, img.getWidth(), img.getHeight()));
}
}

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>
2 changes: 2 additions & 0 deletions modules/grid/build.gradle
Expand Up @@ -2,6 +2,8 @@ dependencies {
testImplementation project(':statics')
testImplementation project(':statics').sourceSets.test.output
testImplementation project(':modules:core').sourceSets.test.output
testImplementation project(':modules:full-screenshot')
testImplementation project(':modules:full-screenshot').sourceSets.test.output

testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
Expand Down
@@ -0,0 +1,58 @@
package integration;

import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.Selenide;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.OutputType;

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

import static com.codeborne.selenide.Selenide.webdriver;
import static com.codeborne.selenide.WebDriverConditions.title;
import static com.codeborne.selenide.WebDriverRunner.isEdge;
import static integration.ScreenshotTestHelper.verifyScreenshotSize;
import static org.assertj.core.api.Assumptions.assumeThat;

public class FullScreenshotsGridTest extends AbstractGridTest {
private static final int width = 2200;
private static final int height = 3300;

@BeforeAll
static void beforeAll() {
assumeThat(isEdge()).as("Edge throws 'unknown command: session/*/goog/cdp/execute'").isFalse();
}

@BeforeEach
void setUp() {
Configuration.remote = "http://localhost:" + hubPort + "/wd/hub";
}

@AfterEach
void tearDown() {
Configuration.remote = null;
}

/*
In non-local browser (grid),
It fails or takes a screenshot of the wrong tab.
See https://github.com/SeleniumHQ/selenium/issues/10810
*/
@Test
void canTakeFullScreenshotWithTwoTabs() throws IOException {
openFile("page_of_fixed_size_2200x3300.html");
webdriver().shouldHave(title("Test::page of fixed size 2200x3300"));

Selenide.executeJavaScript("window.open()");
Selenide.switchTo().window(1);
openFile("file_upload_form.html");

Selenide.switchTo().window(0);

File screenshot = Selenide.screenshot(OutputType.FILE);
verifyScreenshotSize(screenshot, width, height);
}
}
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'