diff --git a/common/src/web/screen/screen.css b/common/src/web/screen/screen.css index 815261850c531..2ad37ce05a586 100644 --- a/common/src/web/screen/screen.css +++ b/common/src/web/screen/screen.css @@ -1,7 +1,7 @@ * { margin: 0; } -html, body, #output { +#output { width: 100%; height: 100%; } diff --git a/java/client/src/org/openqa/selenium/firefox/FirefoxDriver.java b/java/client/src/org/openqa/selenium/firefox/FirefoxDriver.java index 02b7d625786c9..9dddb24ccaa2a 100644 --- a/java/client/src/org/openqa/selenium/firefox/FirefoxDriver.java +++ b/java/client/src/org/openqa/selenium/firefox/FirefoxDriver.java @@ -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; @@ -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; @@ -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 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 { @@ -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 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 getFullPageScreenshotAs(OutputType 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) { diff --git a/java/client/test/org/openqa/selenium/firefox/FirefoxSpecificTests.java b/java/client/test/org/openqa/selenium/firefox/FirefoxSpecificTests.java index 9e08f4f47e873..453ccf5c4f9e0 100644 --- a/java/client/test/org/openqa/selenium/firefox/FirefoxSpecificTests.java +++ b/java/client/test/org/openqa/selenium/firefox/FirefoxSpecificTests.java @@ -28,6 +28,7 @@ MarionetteTest.class, PreferencesTest.class, ExecutableTest.class, + TakesFullPageScreenshotTest.class }) public class FirefoxSpecificTests { diff --git a/java/client/test/org/openqa/selenium/firefox/TakesFullPageScreenshotTest.java b/java/client/test/org/openqa/selenium/firefox/TakesFullPageScreenshotTest.java new file mode 100644 index 0000000000000..d07f401359a1d --- /dev/null +++ b/java/client/test/org/openqa/selenium/firefox/TakesFullPageScreenshotTest.java @@ -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 actualColors = scanActualColors(screenshot, + /* stepX in pixels */ 5, + /* stepY in pixels */ 5); + + Set 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 actualColors = scanActualColors(screenshot, + /* stepX in pixels */ 5, + /* stepY in pixels */ 50); + + Set 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 generateExpectedColors(final int initialColor, final int stepColor, + final int nX, final int nY) { + Set 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 scanActualColors(BufferedImage image, final int stepX, final int stepY) { + Set 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 expectedColors, Set actualColors) { + assertThat(onlyBlack(actualColors)).as("Only black").isFalse(); + assertThat(onlyWhite(actualColors)).as("Only white").isFalse(); + + // Ignore black and white for further comparison + Set 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 colors) { + return colors.size() == 1 && "#000000".equals(colors.toArray()[0]); + } + + private boolean onlyWhite(Set 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()); + } + } + +}