Skip to content

Commit

Permalink
[java] add full page screenshot feature for Firefox (#7295)
Browse files Browse the repository at this point in the history
Add FirefoxDriver calling /session/{session id}/moz/screenshot/full
  • Loading branch information
takeyaqa authored and shs96c committed Jun 20, 2019
1 parent dc8b45a commit 6311d01
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 2 deletions.
2 changes: 1 addition & 1 deletion common/src/web/screen/screen.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
* {
margin: 0;
}
html, body, #output {
#output {
width: 100%;
height: 100%;
}
Expand Down
31 changes: 30 additions & 1 deletion java/client/src/org/openqa/selenium/firefox/FirefoxDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.ImmutableCapabilities;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Proxy;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.html5.LocalStorage;
Expand All @@ -36,6 +37,7 @@
import org.openqa.selenium.remote.CommandInfo;
import org.openqa.selenium.remote.FileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.Response;
import org.openqa.selenium.remote.html5.RemoteWebStorage;
import org.openqa.selenium.remote.http.HttpMethod;
import org.openqa.selenium.remote.service.DriverCommandExecutor;
Expand Down Expand Up @@ -105,13 +107,16 @@ public static final class SystemProperty {
private static class ExtraCommands {
static String INSTALL_EXTENSION = "installExtension";
static String UNINSTALL_EXTENSION = "uninstallExtension";
static String FULL_PAGE_SCREENSHOT = "fullPageScreenshot";
}

private static final ImmutableMap<String, CommandInfo> EXTRA_COMMANDS = ImmutableMap.of(
ExtraCommands.INSTALL_EXTENSION,
new CommandInfo("/session/:sessionId/moz/addon/install", HttpMethod.POST),
ExtraCommands.UNINSTALL_EXTENSION,
new CommandInfo("/session/:sessionId/moz/addon/uninstall", HttpMethod.POST)
new CommandInfo("/session/:sessionId/moz/addon/uninstall", HttpMethod.POST),
ExtraCommands.FULL_PAGE_SCREENSHOT,
new CommandInfo("/session/:sessionId/moz/screenshot/full", HttpMethod.GET)
);

private static class FirefoxDriverCommandExecutor extends DriverCommandExecutor {
Expand Down Expand Up @@ -214,6 +219,30 @@ public void uninstallExtension(String extensionId) {
execute(ExtraCommands.UNINSTALL_EXTENSION, singletonMap("id", extensionId));
}

/**
* Capture the full page screenshot and store it in the specified location.
*
* @param <X> Return type for getFullPageScreenshotAs.
* @param outputType target type, @see OutputType
* @return Object in which is stored information about the screenshot.
* @throws WebDriverException on failure.
*/
public <X> X getFullPageScreenshotAs(OutputType<X> outputType) throws WebDriverException {
Response response = execute(ExtraCommands.FULL_PAGE_SCREENSHOT);
Object result = response.getValue();
if (result instanceof String) {
String base64EncodedPng = (String) result;
return outputType.convertFromBase64Png(base64EncodedPng);
} else if (result instanceof byte[]) {
String base64EncodedPng = new String((byte[]) result);
return outputType.convertFromBase64Png(base64EncodedPng);
} else {
throw new RuntimeException(String.format("Unexpected result for %s command: %s",
ExtraCommands.FULL_PAGE_SCREENSHOT,
result == null ? "null" : result.getClass().getName() + " instance"));
}
}

private static Boolean forceMarionetteFromSystemProperty() {
String useMarionette = System.getProperty(SystemProperty.DRIVER_USE_MARIONETTE);
if (useMarionette == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
MarionetteTest.class,
PreferencesTest.class,
ExecutableTest.class,
TakesFullPageScreenshotTest.class
})

public class FirefoxSpecificTests {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.firefox;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import com.google.common.collect.Sets;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.testing.JUnit4TestBase;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.TreeSet;

import javax.imageio.ImageIO;

/**
* Test screenshot feature.
*
* 1. check output for all possible types
*
* 2. check screenshot image
*
* Logic of screenshot check test is simple:
* - open page with fixed amount of fixed sized and coloured areas
* - take screenshot
* - calculate expected colors as in tested HTML page
* - scan screenshot for actual colors * compare
*
* @see org.openqa.selenium.TakesScreenshotTest
*
*/

// TODO(user): verify expected behaviour after frame switching

// TODO(user): test screenshots at guaranteed maximized browsers
// TODO(user): test screenshots at guaranteed non maximized browsers
// TODO(user): test screenshots at guaranteed minimized browsers
// TODO(user): test screenshots at guaranteed fullscreened/kiosked browsers (WINDOWS platform specific)

public class TakesFullPageScreenshotTest extends JUnit4TestBase {

private FirefoxDriver screenshooter;
private File tempFile = null;

@Before
public void setUp() {
assumeTrue(driver instanceof FirefoxDriver);
screenshooter = (FirefoxDriver) driver;
}

@After
public void tearDown() {
if (tempFile != null) {
tempFile.delete();
tempFile = null;
}
}

@Test
public void testGetScreenshotAsFile() {
driver.get(pages.simpleTestPage);
tempFile = screenshooter.getFullPageScreenshotAs(OutputType.FILE);
assertThat(tempFile.exists()).isTrue();
assertThat(tempFile.length()).isGreaterThan(0);
}

@Test
public void testGetScreenshotAsBase64() {
driver.get(pages.simpleTestPage);
String screenshot = screenshooter.getFullPageScreenshotAs(OutputType.BASE64);
assertThat(screenshot.length()).isGreaterThan(0);
}

@Test
public void testGetScreenshotAsBinary() {
driver.get(pages.simpleTestPage);
byte[] screenshot = screenshooter.getFullPageScreenshotAs(OutputType.BYTES);
assertThat(screenshot.length).isGreaterThan(0);
}

@Test
public void testShouldCaptureScreenshotOfCurrentViewport() {
driver.get(appServer.whereIs("screen/screen.html"));

BufferedImage screenshot = getImage();

Set<String> actualColors = scanActualColors(screenshot,
/* stepX in pixels */ 5,
/* stepY in pixels */ 5);

Set<String> expectedColors = generateExpectedColors( /* initial color */ 0x0F0F0F,
/* color step */ 1000,
/* grid X size */ 6,
/* grid Y size */ 6);

compareColors(expectedColors, actualColors);
}

@Test
public void testShouldCaptureScreenshotOfPageWithLongY() {
driver.get(appServer.whereIs("screen/screen_y_long.html"));

BufferedImage screenshot = getImage();

Set<String> actualColors = scanActualColors(screenshot,
/* stepX in pixels */ 5,
/* stepY in pixels */ 50);

Set<String> expectedColors = generateExpectedColors( /* initial color */ 0x0F0F0F,
/* color step*/ 1000,
/* grid X size */ 6,
/* grid Y size */ 6);

compareColors(expectedColors, actualColors);
}

/**
* get actual image screenshot
*
* @return Image object
*/
private BufferedImage getImage() {
BufferedImage image = null;
try {
byte[] imageData = screenshooter.getFullPageScreenshotAs(OutputType.BYTES);
assertThat(imageData).isNotNull();
assertThat(imageData.length).isGreaterThan(0);
image = ImageIO.read(new ByteArrayInputStream(imageData));
assertThat(image).isNotNull();
} catch (IOException e) {
fail("Image screenshot file is invalid: " + e.getMessage());
}

//saveImageToTmpFile(image);
return image;
}

/**
* generate expected colors as in checked page.
*
* @param initialColor - initial color of first (right top) cell of grid
* @param stepColor - step b/w grid colors as number
* @param nX - grid size at X dimension
* @param nY - grid size at Y dimension
* @return set of colors in string hex presentation
*/
private Set<String> generateExpectedColors(final int initialColor, final int stepColor,
final int nX, final int nY) {
Set<String> colors = new TreeSet<>();
int cnt = 1;
for (int i = 1; i < nX; i++) {
for (int j = 1; j < nY; j++) {
int color = initialColor + (cnt * stepColor);
String hex =
String.format("#%02x%02x%02x", ((color & 0xFF0000) >> 16), ((color & 0x00FF00) >> 8),
((color & 0x0000FF)));
colors.add(hex);
cnt++;
}
}

return colors;
}

/**
* Get colors from image from each point at grid defined by stepX/stepY.
*
* @param image - image
* @param stepX - interval in pixels b/w point in X dimension
* @param stepY - interval in pixels b/w point in Y dimension
* @return set of colors in string hex presentation
*/
private Set<String> scanActualColors(BufferedImage image, final int stepX, final int stepY) {
Set<String> colors = new TreeSet<>();

try {
int height = image.getHeight();
int width = image.getWidth();
assertThat(width > 0).isTrue();
assertThat(height > 0).isTrue();

Raster raster = image.getRaster();
for (int i = 0; i < width; i = i + stepX) {
for (int j = 0; j < height; j = j + stepY) {
String hex = String.format("#%02x%02x%02x",
(raster.getSample(i, j, 0)),
(raster.getSample(i, j, 1)),
(raster.getSample(i, j, 2)));
colors.add(hex);
}
}
} catch (Exception e) {
fail("Unable to get actual colors from screenshot: " + e.getMessage());
}

assertThat(colors).isNotEmpty();

return colors;
}

/**
* Compares sets of colors are same.
*
* @param expectedColors - set of expected colors
* @param actualColors - set of actual colors
*/
private void compareColors(Set<String> expectedColors, Set<String> actualColors) {
assertThat(onlyBlack(actualColors)).as("Only black").isFalse();
assertThat(onlyWhite(actualColors)).as("Only white").isFalse();

// Ignore black and white for further comparison
Set<String> cleanActualColors = Sets.newHashSet(actualColors);
cleanActualColors.remove("#000000");
cleanActualColors.remove("#ffffff");

if (! expectedColors.containsAll(cleanActualColors)) {
fail("There are unexpected colors on the screenshot: " +
Sets.difference(cleanActualColors, expectedColors));
}

if (! cleanActualColors.containsAll(expectedColors)) {
fail("There are expected colors not present on the screenshot: " +
Sets.difference(expectedColors, cleanActualColors));
}
}

private boolean onlyBlack(Set<String> colors) {
return colors.size() == 1 && "#000000".equals(colors.toArray()[0]);
}

private boolean onlyWhite(Set<String> colors) {
return colors.size() == 1 && "#ffffff".equals(colors.toArray()[0]);
}

/**
* Simple helper to save screenshot to tmp file. For debug purposes.
*
* @param im image
*/
@SuppressWarnings("unused")
private void saveImageToTmpFile(BufferedImage im) {

File outputfile = new File( testName.getMethodName() + "_image.png");
try {
ImageIO.write(im, "png", outputfile);
} catch (IOException e) {
fail("Unable to write image to file: " + e.getMessage());
}
}

}

0 comments on commit 6311d01

Please sign in to comment.