Repository: pazone/ashot Branch: master Commit: 0fcb25888aa3 Files: 68 Total size: 157.5 KB Directory structure: gitextract_ur45qq_0/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── .mvn/ │ └── wrapper/ │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENCE ├── README.md ├── config/ │ └── checkstyle/ │ └── checkstyle.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── pazone/ │ │ └── ashot/ │ │ ├── AShot.java │ │ ├── CdpShootingStrategy.java │ │ ├── CuttingDecorator.java │ │ ├── HandlingSickyElementsViewportPastingDecorator.java │ │ ├── ImageReadException.java │ │ ├── InvalidViewportHeightException.java │ │ ├── PageDimensions.java │ │ ├── RotatingDecorator.java │ │ ├── ScalingDecorator.java │ │ ├── Screenshot.java │ │ ├── ShootingDecorator.java │ │ ├── ShootingStrategies.java │ │ ├── ShootingStrategy.java │ │ ├── SimpleShootingStrategy.java │ │ ├── ViewportPastingDecorator.java │ │ ├── comparison/ │ │ │ ├── DiffMarkupPolicy.java │ │ │ ├── ImageDiff.java │ │ │ ├── ImageDiffer.java │ │ │ ├── ImageMarkupPolicy.java │ │ │ └── PointsMarkupPolicy.java │ │ ├── coordinates/ │ │ │ ├── Coords.java │ │ │ ├── CoordsPreparationStrategy.java │ │ │ ├── CoordsProvider.java │ │ │ ├── JqueryCoordsProvider.java │ │ │ └── WebDriverCoordsProvider.java │ │ ├── cropper/ │ │ │ ├── DefaultCropper.java │ │ │ ├── ImageCropper.java │ │ │ └── indent/ │ │ │ ├── BlurFilter.java │ │ │ ├── IndentCropper.java │ │ │ ├── IndentFilerFactory.java │ │ │ ├── IndentFilter.java │ │ │ └── MonochromeFilter.java │ │ ├── cutter/ │ │ │ ├── CutStrategy.java │ │ │ ├── FixedCutStrategy.java │ │ │ └── VariableCutStrategy.java │ │ └── util/ │ │ ├── ImageBytesDiffer.java │ │ ├── ImageTool.java │ │ ├── InnerScript.java │ │ └── JsCoords.java │ └── resources/ │ └── js/ │ ├── coords-single.js │ └── page_dimensions.js └── test/ └── java/ └── pazone/ └── ashot/ ├── CdpShootingStrategyTest.java ├── CroppersTest.java ├── CuttingDecoratorTest.java ├── DiffMarkupPolicyTest.java ├── DifferTest.java ├── ImageBytesDifferTest.java ├── RotatingDecoratorTest.java ├── ScalingDecoratorTest.java ├── SerializeScreenshotTest.java ├── VariableCutStrategyTest.java ├── VerticalPastingShootingStrategyTest.java ├── coordinates/ │ └── WebDriverCoordsProviderTest.java └── util/ ├── ImageToolTest.java └── TestImageUtils.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: maven directory: "/" schedule: interval: weekly open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: "/" schedule: interval: weekly open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: branches: - master jobs: build: runs-on: ubuntu-latest strategy: matrix: java: [ 11, 17, 21, 25 ] steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: ${{ matrix.java }} cache: 'maven' - name: Tests run: ./mvnw clean test ================================================ FILE: .github/workflows/release.yml ================================================ name: Release and Deploy on: workflow_dispatch jobs: release: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: '11' server-id: github server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD - name: Release run: | git config user.email "pazonec@yandex.ru" git config --global user.name "Pavel Zorin" ./mvnw --batch-mode -DskipTests -X release:clean release:prepare release:perform env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} MAVEN_USERNAME: ${{ secrets.GITHUB_ACTOR }} MAVEN_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/snapshot.yml ================================================ name: Deploy Snapshot on: push: branches: - master jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 11 cache: 'maven' server-id: github server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD - name: deploy snapshot (github packages) if: github.ref == 'refs/heads/master' env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} MAVEN_USERNAME: ${{ secrets.GITHUB_ACTOR }} MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} run: ./mvnw --batch-mode -Dgpg.passphrase=$GPG_PASSPHRASE -DskipTests deploy ================================================ FILE: .gitignore ================================================ target .git .idea *.iml ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF 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. wrapperVersion=3.3.2 distributionType=bin distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar ================================================ FILE: LICENCE ================================================ Copyright 2014 YANDEX Licensed 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. ================================================ FILE: README.md ================================================ aShot ===== [![release](https://img.shields.io/github/v/release/pazone/ashot.svg)](https://github.com/pazone/ashot/releases/latest) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/ru.yandex.qatools.ashot/ashot/badge.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.yandex.qatools.ashot/ashot) WebDriver Screenshot utility * Takes a screenshot of a WebElement on different platforms (i.e. desktop browsers, iOS Simulator Mobile Safari, Android Emulator Browser) * Decorates screenshots * Provides flexible screenshot comparison ##### WebElement view aShot takes a screenshot in three simple steps: * Capture a screenshot of the entire page * Find the element's size and position * Crop the original screenshot As a result, aShot provides an image of the WebElement ![images snippet](/doc/img/images_intent_blur.png) #### Maven dependency ```xml ru.yandex.qatools.ashot ashot 1.5.4 ``` ##### Capturing the entire page Different WebDrivers take screenshots differently. Some WebDrivers provide a screenshot of the entire page while others handle the viewport only. aShot might be configured to handle browsers with the viewport problem. This example configuration gives a screenshot of the entire page even for Chrome, Mobile Safari, etc. ```java new AShot() .shootingStrategy(ShootingStrategies.viewportPasting(100)) .takeScreenshot(webDriver); ``` There are built-in strategies in `ShootingStrategies` for different use cases. In case there are no suitable strategies it is possible to build it using existing strategies as decorators or implement your own. ```java CutStrategy cutting = new VariableCutStrategy(HEADER_IOS_8_MIN, HEADER_IOS_8_MAX, VIEWPORT_MIN_IOS_8_SIM); ShootingStrategy rotating = new RotatingDecorator(cutting, ShootingStrategies.simple()); ShootingStrategy pasting = new ViewportPastingDecorator(rotating) .withScrollTimeout(scrollTimeout); new AShot() .shootingStrategy(pasting) .takeScreenshot(webDriver); ``` ##### Capturing the WebElement One can take a screenshot of particular WebElement(s). Just specify the element(s). ```java WebElement myWebElement = webDriver.findElement(By.cssSelector("#my_element")); new AShot() .takeScreenshot(webDriver, myWebElement); ``` As noted earlier, aShot will find an element's size and position and crop the original image. WebDriver API provides a method to find the WebElement's coordinates but different WebDriver implementations behave differently. The most general approach to find coordinates is to use jQuery, so aShot uses jQuery by default. But some drivers have problems with Javascript execution such as OperaDriver. In this case there is another way to find the WebElement's coordinates. ```java new AShot() .coordsProvider(new WebDriverCoordsProvider()) //find coordinates with WebDriver API .takeScreenshot(webDriver, myWebElement); ``` Feel free to implement your own CoordsProvider. Pull requests are welcome. ##### Prettifying the screenshot So, let's take a simple screenshot of the weather snippet at Yandex.com. ```java new AShot() .takeScreenshot(webDriver, yandexWeatherElement); ``` Here is the result. ![simple weather snippet](/doc/img/def_crop.png) `DefaultCropper` is used by default. Can we do better? Yes, we can. ```java new AShot() .withCropper(new IndentCropper() // set custom cropper with indentation .addIndentFilter(blur())) // add filter for indented areas .takeScreenshot(driver, yandexWeatherElement); ``` ![indent blur weather snippet](/doc/img/weather_indent_blur.png) This screenshot provides more information about position relative other elements and blurs indent in order to focus on the WebElement. ##### Screenshot comparison ```.takeScreenshot()``` returns a ```Screenshot``` object which contains an image and data for comparison. One can ignore some WebElements from comparison. ```java Screenshot myScreenshot = new AShot() .addIgnoredElement(By.cssSelector("#weather .blinking_element")) // ignored element(s) .takeScreenshot(driver, yandexWeatherElement); ``` Use `ImageDiffer` to find a difference between two images. ```java ImageDiff diff = new ImageDiffer().makeDiff(myScreenshot, anotherScreenshot); BufferedImage diffImage = diff.getMarkedImage(); // comparison result with marked differences ``` ##### Several elements comparison `(since 1.2)` Sometimes one needs to take a screenshot of several independent elements. In this case aShot computes complex comparison area. ```java List elements = webDriver.findElements(By.cssSelector("#my_element, #popup")); new AShot() .withCropper(new IndentCropper() .addIndentFilter(blur())) .takeScreenshot(webDriver, elements); ``` Here is the result. ![complex comparison area](/doc/img/complex_elements.png) One can see only specified elements (the header and the popup) are focused and will be compared if needed. ##### Ignoring of pixels with predefined color You can set the color of pixels which should be excluded from comparison of screenshots. ```java ImageDiffer imageDifferWithIgnored = new ImageDiffer().withIgnoredColor(Color.MAGENTA); ImageDiff diff = imageDifferWithIgnored.makeDiff(templateWithSomeMagentaPixels, actualScreenshot); assertFalse(diff.hasDiff()); ``` Any pixels in template with color `MAGENTA` (255, 0, 255 in RGB) will be ignored during comparison. ================================================ FILE: config/checkstyle/checkstyle.xml ================================================ ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF 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. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.3.2 # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ]; then if [ -f /usr/local/etc/mavenrc ]; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ]; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ]; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false darwin=false mingw=false case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true ;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then JAVA_HOME="$(/usr/libexec/java_home)" export JAVA_HOME else JAVA_HOME="/Library/Java/Home" export JAVA_HOME fi fi ;; esac if [ -z "$JAVA_HOME" ]; then if [ -r /etc/gentoo-release ]; then JAVA_HOME=$(java-config --jre-home) fi fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin; then [ -n "$JAVA_HOME" ] \ && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] \ && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw; then [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ && JAVA_HOME="$( cd "$JAVA_HOME" || ( echo "cannot cd into $JAVA_HOME." >&2 exit 1 ) pwd )" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="$(which javac)" if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=$(which readlink) if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin; then javaHome="$(dirname "$javaExecutable")" javaExecutable="$(cd "$javaHome" && pwd -P)/javac" else javaExecutable="$(readlink -f "$javaExecutable")" fi javaHome="$(dirname "$javaExecutable")" javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ]; then if [ -n "$JAVA_HOME" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="$( \unset -f command 2>/dev/null \command -v java )" fi fi if [ ! -x "$JAVACMD" ]; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ]; then echo "Warning: JAVA_HOME environment variable is not set." >&2 fi # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ]; then echo "Path not specified to find_maven_basedir" >&2 return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ]; do if [ -d "$wdir"/.mvn ]; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=$( cd "$wdir/.." || exit 1 pwd ) fi # end of workaround done printf '%s' "$( cd "$basedir" || exit 1 pwd )" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then # Remove \r in case we run on Windows within Git Bash # and check out the repository with auto CRLF management # enabled. Otherwise, we may read lines that are delimited with # \r\n and produce $'-Xarg\r' rather than -Xarg due to word # splitting rules. tr -s '\r\n' ' ' <"$1" fi } log() { if [ "$MVNW_VERBOSE" = true ]; then printf '%s\n' "$1" fi } BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1 fi MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} export MAVEN_PROJECTBASEDIR log "$MAVEN_PROJECTBASEDIR" ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" if [ -r "$wrapperJarPath" ]; then log "Found $wrapperJarPath" else log "Couldn't find $wrapperJarPath, downloading it ..." if [ -n "$MVNW_REPOURL" ]; then wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" else wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" fi while IFS="=" read -r key value; do # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) safeValue=$(echo "$value" | tr -d '\r') case "$key" in wrapperUrl) wrapperUrl="$safeValue" break ;; esac done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" log "Downloading from: $wrapperUrl" if $cygwin; then wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") fi if command -v wget >/dev/null; then log "Found wget ... using wget" [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl >/dev/null; then log "Found curl ... using curl" [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" else curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" fi else log "Falling back to using Java to download" javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaSource=$(cygpath --path --windows "$javaSource") javaClass=$(cygpath --path --windows "$javaClass") fi if [ -e "$javaSource" ]; then if [ ! -e "$javaClass" ]; then log " - Compiling MavenWrapperDownloader.java ..." ("$JAVA_HOME/bin/javac" "$javaSource") fi if [ -e "$javaClass" ]; then log " - Running MavenWrapperDownloader.java ..." ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" fi fi fi fi ########################################################################################## # End of extension ########################################################################################## # If specified, validate the SHA-256 sum of the Maven wrapper jar file wrapperSha256Sum="" while IFS="=" read -r key value; do case "$key" in wrapperSha256Sum) wrapperSha256Sum=$value break ;; esac done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" if [ -n "$wrapperSha256Sum" ]; then wrapperSha256Result=false if command -v sha256sum >/dev/null; then if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then wrapperSha256Result=true fi elif command -v shasum >/dev/null; then if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then wrapperSha256Result=true fi else echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi if [ $wrapperSha256Result = false ]; then echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 exit 1 fi fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$JAVA_HOME" ] \ && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] \ && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] \ && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain # shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. >&2 echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. >&2 goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. >&2 echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. >&2 goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %WRAPPER_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file SET WRAPPER_SHA_256_SUM="" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B ) IF NOT %WRAPPER_SHA_256_SUM%=="" ( powershell -Command "&{"^ "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ " exit 1;"^ "}"^ "}" if ERRORLEVEL 1 goto error ) @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: pom.xml ================================================ 4.0.0 io.github.pazone ashot 1.6.1-SNAPSHOT The Apache Software License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt repo GitHub Issues https://github.com/pazone/ashot/issues https://github.com/pazone/ashot scm:git:git@github.com:pazone/ashot.git scm:git:git@github.com:pazone/ashot.git HEAD UTF-8 4.43.0 AShot WebDriver Utility org.seleniumhq.selenium selenium-remote-driver ${selenium.version} org.seleniumhq.selenium selenium-chromium-driver ${selenium.version} true commons-io commons-io 2.21.0 com.google.code.gson gson 2.13.2 org.hamcrest hamcrest 3.0 org.junit.jupiter junit-jupiter 5.14.3 test org.mockito mockito-junit-jupiter 5.23.0 test org.apache.maven.plugins maven-source-plugin 3.4.0 attach-sources jar org.apache.maven.plugins maven-compiler-plugin 3.15.0 11 11 true org.apache.maven.plugins maven-surefire-plugin 3.5.5 org.apache.maven.plugins maven-javadoc-plugin 3.12.0 -Xdoclint:none org.apache.maven.plugins maven-release-plugin 3.3.1 [ci skip] github https://maven.pkg.github.com/pazone/ashot checkstyle-checks [21,) org.apache.maven.plugins maven-checkstyle-plugin 3.6.0 config/checkstyle/checkstyle.xml true true true validate validate check com.puppycrawl.tools checkstyle 13.4.0 release-sign-artifacts performSigning true org.apache.maven.plugins maven-gpg-plugin 3.2.8 sign-artifacts verify sign ================================================ FILE: src/main/java/pazone/ashot/AShot.java ================================================ package pazone.ashot; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import pazone.ashot.coordinates.Coords; import pazone.ashot.coordinates.CoordsPreparationStrategy; import pazone.ashot.coordinates.CoordsProvider; import pazone.ashot.coordinates.WebDriverCoordsProvider; import pazone.ashot.cropper.ImageCropper; import pazone.ashot.cropper.DefaultCropper; import java.awt.image.BufferedImage; import java.io.Serializable; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import static java.util.Collections.singletonList; /** * @author Pavel Zorin */ public class AShot implements Serializable { private CoordsProvider coordsProvider = new WebDriverCoordsProvider(); private ImageCropper cropper = new DefaultCropper(); private Set ignoredLocators = new HashSet<>(); private Set ignoredAreas = new HashSet<>(); private ShootingStrategy shootingStrategy = new SimpleShootingStrategy(); public AShot coordsProvider(final CoordsProvider coordsProvider) { this.coordsProvider = coordsProvider; return this; } @SuppressWarnings("UnusedDeclaration") public AShot imageCropper(ImageCropper cropper) { this.cropper = cropper; return this; } /** * Sets the list of locators to ignore during image comparison. * * @param ignoredElements list of By * @return this */ @SuppressWarnings("UnusedDeclaration") public synchronized AShot ignoredElements(final Set ignoredElements) { this.ignoredLocators = ignoredElements; return this; } /** * Adds selector of ignored element. * * @param selector By * @return this */ public synchronized AShot addIgnoredElement(final By selector) { this.ignoredLocators.add(selector); return this; } /** * Sets a collection of wittingly ignored coords. * @param ignoredAreas Set of ignored areas * @return aShot */ @SuppressWarnings("UnusedDeclaration") public synchronized AShot ignoredAreas(final Set ignoredAreas) { this.ignoredAreas = ignoredAreas; return this; } /** * Adds coordinated to set of wittingly ignored coords. * @param area coords of wittingly ignored coords * @return aShot; */ @SuppressWarnings("UnusedDeclaration") public synchronized AShot addIgnoredArea(Coords area) { this.ignoredAreas.add(area); return this; } /** * Sets the policy of taking screenshot. * * @param strategy shooting strategy * @return this * @see ShootingStrategy */ public AShot shootingStrategy(ShootingStrategy strategy) { this.shootingStrategy = strategy; return this; } /** * Takes the screenshot of given elements * If elements were not found screenshot of whole page will be returned * * @param driver WebDriver instance * @param elements Web elements to screenshot * @return Screenshot with cropped image and list of ignored areas on screenshot * @throws RuntimeException when something goes wrong * @see Screenshot */ public Screenshot takeScreenshot(WebDriver driver, Collection elements) { Set elementCoords = coordsProvider.ofElements(driver, elements); BufferedImage shot = shootingStrategy.getScreenshot(driver, elementCoords); Screenshot screenshot = cropper.crop(shot, shootingStrategy.prepareCoords(elementCoords)); Set ignoredAreas = compileIgnoredAreas(driver, CoordsPreparationStrategy.intersectingWith(screenshot)); screenshot.setIgnoredAreas(shootingStrategy.prepareCoords(ignoredAreas)); return screenshot; } /** * Takes the screenshot of given element * * @param driver WebDriver instance * @param element Web element to screenshot * @return Screenshot with cropped image and list of ignored areas on screenshot * @throws RuntimeException when something goes wrong * @see Screenshot */ public Screenshot takeScreenshot(WebDriver driver, WebElement element) { return takeScreenshot(driver, singletonList(element)); } /** * Takes the screenshot of whole page * * @param driver WebDriver instance * @return Screenshot with whole page image and list of ignored areas on screenshot * @see Screenshot */ public Screenshot takeScreenshot(WebDriver driver) { Screenshot screenshot = new Screenshot(shootingStrategy.getScreenshot(driver)); screenshot.setIgnoredAreas(compileIgnoredAreas(driver, CoordsPreparationStrategy.simple())); return screenshot; } protected synchronized Set compileIgnoredAreas(WebDriver driver, CoordsPreparationStrategy preparationStrategy) { Set ignoredCoords = new HashSet<>(); for (By ignoredLocator : ignoredLocators) { List ignoredElements = driver.findElements(ignoredLocator); if (!ignoredElements.isEmpty()) { ignoredCoords.addAll(preparationStrategy.prepare(coordsProvider.ofElements(driver, ignoredElements))); } } for (Coords ignoredArea : ignoredAreas) { ignoredCoords.addAll(preparationStrategy.prepare(singletonList(ignoredArea))); } return ignoredCoords; } @SuppressWarnings("UnusedDeclaration") public synchronized Set getIgnoredLocators() { return ignoredLocators; } } ================================================ FILE: src/main/java/pazone/ashot/CdpShootingStrategy.java ================================================ package pazone.ashot; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.UncheckedIOException; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.openqa.selenium.OutputType; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chromium.HasCdp; import pazone.ashot.coordinates.Coords; import pazone.ashot.util.ImageTool; /** * Gets a screenshot using * * capture screenshot function provided by Chrome DevTools protocol. {@link WebDriver} instance provided * to the class methods must be an instance of {@link HasCdp} and support Chrome DevTools protocol. */ public class CdpShootingStrategy implements ShootingStrategy { private static final long serialVersionUID = -4371668803381640029L; @Override public BufferedImage getScreenshot(WebDriver driver) { return getScreenshot(driver, Set.of()); } @Override public BufferedImage getScreenshot(WebDriver driver, Set coords) { if (!HasCdp.class.isAssignableFrom(driver.getClass())) { throw new IllegalArgumentException("WebDriver instance must support Chrome DevTools protocol"); } Map args = new HashMap<>(); args.put("captureBeyondViewport", true); if (!coords.isEmpty()) { Coords elementCoords = coords.iterator().next(); args.put("clip", Map.of( "x", elementCoords.x, "y", elementCoords.y, "width", elementCoords.width, "height", elementCoords.height, "scale", 1) ); } Map results = ((HasCdp) driver).executeCdpCommand("Page.captureScreenshot", args); String base64 = (String) results.get("data"); byte[] bytes = OutputType.BYTES.convertFromBase64Png(base64); try { return ImageTool.toBufferedImage(bytes); } catch (IOException thrown) { throw new UncheckedIOException(thrown); } } @Override public Set prepareCoords(Set coordsSet) { return coordsSet; } } ================================================ FILE: src/main/java/pazone/ashot/CuttingDecorator.java ================================================ package pazone.ashot; import org.openqa.selenium.WebDriver; import pazone.ashot.cutter.CutStrategy; import pazone.ashot.cutter.FixedCutStrategy; import pazone.ashot.coordinates.Coords; import java.awt.image.BufferedImage; import java.util.Set; /** * Cuts browser's header/footer off from screenshot. * * @author Pavel Zorin */ public class CuttingDecorator extends ShootingDecorator { private CutStrategy cutStrategy; public CuttingDecorator(ShootingStrategy strategy) { super(strategy); } /** * Will use {@link FixedCutStrategy} to cut off header and footer. * @param headerToCut - height of header in pixels * @param footerToCut - height of footer in pixels * @return Cutting decorator */ public CuttingDecorator withCut(int headerToCut, int footerToCut) { return withCutStrategy(new FixedCutStrategy(headerToCut, footerToCut)); } /** * Will use {@link FixedCutStrategy} to cut off header and footer. * @param headerToCut - height of header in pixels * @param footerToCut - height of footer in pixels * @param leftBarToCut - width of left bar in pixels * @param rightBarToCut - width of right bar in pixels * @return Cutting decorator */ public CuttingDecorator withCut(int headerToCut, int footerToCut, int leftBarToCut, int rightBarToCut) { return withCutStrategy(new FixedCutStrategy(headerToCut, footerToCut, leftBarToCut, rightBarToCut)); } /** * Will use custom cut strategy, for example {@link pazone.ashot.cutter.VariableCutStrategy}. * @param cutStrategy - strategy to get height of browser's header * @return Cutting decorator */ public CuttingDecorator withCutStrategy(CutStrategy cutStrategy) { this.cutStrategy = cutStrategy; return this; } @Override public BufferedImage getScreenshot(WebDriver wd) { BufferedImage baseImage = getShootingStrategy().getScreenshot(wd); int h = baseImage.getHeight(); int w = baseImage.getWidth(); final int headerToCut = getHeaderToCut(wd); final int footerToCut = getFooterToCut(wd); final int leftBarToCut = getLeftBarToCut(wd); final int rightBarToCut = getRightBarToCut(wd); return baseImage.getSubimage(leftBarToCut, headerToCut, w - leftBarToCut - rightBarToCut, h - headerToCut - footerToCut); } @Override public BufferedImage getScreenshot(WebDriver wd, Set coords) { return getScreenshot(wd); } protected int getHeaderToCut(WebDriver wd) { return cutStrategy.getHeaderHeight(wd); } protected int getFooterToCut(WebDriver wd) { return cutStrategy.getFooterHeight(wd); } protected int getLeftBarToCut(WebDriver wd) { return cutStrategy.getLeftBarWidth(wd); } protected int getRightBarToCut(WebDriver wd) { return cutStrategy.getRightBarWidth(wd); } } ================================================ FILE: src/main/java/pazone/ashot/HandlingSickyElementsViewportPastingDecorator.java ================================================ package pazone.ashot; import java.awt.image.BufferedImage; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; public class HandlingSickyElementsViewportPastingDecorator extends ViewportPastingDecorator { private final int stickyHeaderHeight; private final int stickyFooterHeight; private int currentChunkIndex; public HandlingSickyElementsViewportPastingDecorator(ShootingStrategy strategy, int stickyHeaderHeight, int stickyFooterHeight) { super(strategy); this.stickyHeaderHeight = stickyHeaderHeight; this.stickyFooterHeight = stickyFooterHeight; } @Override protected PageDimensions getPageDimensions(WebDriver driver) { PageDimensions pageDimension = super.getPageDimensions(driver); return new PageDimensions(pageDimension.getPageHeight(), pageDimension.getViewportWidth(), pageDimension.getViewportHeight() - stickyHeaderHeight - stickyFooterHeight); } @Override protected BufferedImage getChunk(WebDriver wd, int currentChunkIndex, int totalNumberOfChunks) { this.currentChunkIndex = currentChunkIndex; CuttingDecorator cuttingDecorator = new CuttingDecorator(getShootingStrategy()); if (currentChunkIndex == 0) { cuttingDecorator.withCut(0, stickyFooterHeight); } else if (currentChunkIndex == totalNumberOfChunks - 1) { cuttingDecorator.withCut(stickyHeaderHeight, 0); } else { cuttingDecorator.withCut(stickyHeaderHeight, stickyFooterHeight); } return cuttingDecorator.getScreenshot(wd); } @Override protected int getCurrentScrollY(JavascriptExecutor js) { int currentScrollY = super.getCurrentScrollY(js); return currentChunkIndex == 0 ? currentScrollY : currentScrollY + stickyHeaderHeight; } } ================================================ FILE: src/main/java/pazone/ashot/ImageReadException.java ================================================ package pazone.ashot; /** * @author Vyacheslav Frolov */ public class ImageReadException extends RuntimeException { public ImageReadException(String message) { super(message); } public ImageReadException(String message, Exception e) { super(message, e); } } ================================================ FILE: src/main/java/pazone/ashot/InvalidViewportHeightException.java ================================================ package pazone.ashot; /** * @author Vyacheslav Frolov */ public class InvalidViewportHeightException extends RuntimeException { public InvalidViewportHeightException(String message) { super(message); } public InvalidViewportHeightException(String message, Exception e) { super(message, e); } } ================================================ FILE: src/main/java/pazone/ashot/PageDimensions.java ================================================ package pazone.ashot; public final class PageDimensions { private final int pageHeight; private final int viewportWidth; private final int viewportHeight; public PageDimensions(int pageHeight, int viewportWidth, int viewportHeight) { this.pageHeight = pageHeight; this.viewportHeight = viewportHeight; this.viewportWidth = viewportWidth; } public int getPageHeight() { return pageHeight; } public int getViewportWidth() { return viewportWidth; } public int getViewportHeight() { return viewportHeight; } } ================================================ FILE: src/main/java/pazone/ashot/RotatingDecorator.java ================================================ package pazone.ashot; import org.openqa.selenium.WebDriver; import pazone.ashot.coordinates.Coords; import pazone.ashot.cutter.CutStrategy; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.Set; import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR; /** * @author Rovniakov Viacheslav */ public class RotatingDecorator implements ShootingStrategy { private final CutStrategy cutStrategy; private final ShootingStrategy shootingStrategy; public RotatingDecorator(CutStrategy cutStrategy, ShootingStrategy shootingStrategy) { this.cutStrategy = cutStrategy; this.shootingStrategy = shootingStrategy; } @Override public BufferedImage getScreenshot(WebDriver wd) { return rotate(shootingStrategy.getScreenshot(wd), wd); } @Override public BufferedImage getScreenshot(WebDriver wd, Set coords) { return getScreenshot(wd); } @Override public Set prepareCoords(Set coordsSet) { return coordsSet; } private BufferedImage rotate(BufferedImage baseImage, WebDriver wd) { BufferedImage rotated = new BufferedImage(baseImage.getHeight(), baseImage.getWidth(), TYPE_4BYTE_ABGR); Graphics2D graphics = rotated.createGraphics(); double theta = 3 * Math.PI / 2; int origin = baseImage.getWidth() / 2; graphics.rotate(theta, origin, origin); graphics.drawImage(baseImage, null, 0, 0); int rotatedHeight = rotated.getHeight(); int rotatedWidth = rotated.getWidth(); int headerToCut = cutStrategy.getHeaderHeight(wd); return rotated.getSubimage(0, headerToCut, rotatedWidth, rotatedHeight - headerToCut); } } ================================================ FILE: src/main/java/pazone/ashot/ScalingDecorator.java ================================================ package pazone.ashot; import org.openqa.selenium.WebDriver; import pazone.ashot.coordinates.Coords; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.util.Set; /** * Will scale down image that was aquired by WebDriver. * Scaling is performed according to device pixel ratio (DPR) * Useful for browsers on portable devices with Retina displays. */ public class ScalingDecorator extends ShootingDecorator { private static final Float STANDARD_DRP = 1F; private Float dprX = STANDARD_DRP; private Float dprY = STANDARD_DRP; public ScalingDecorator(ShootingStrategy strategy) { super(strategy); } @Override public BufferedImage getScreenshot(WebDriver wd) { return scale(getShootingStrategy().getScreenshot(wd)); } @Override public BufferedImage getScreenshot(WebDriver wd, Set coords) { return scale(getShootingStrategy().getScreenshot(wd, coords)); } public ScalingDecorator withDprX(float dprX) { this.dprX = dprX; return this; } public ScalingDecorator withDprY(float dprY) { this.dprY = dprY; return this; } public ScalingDecorator withDpr(float dpr) { return this .withDprX(dpr) .withDprY(dpr); } private BufferedImage scale(BufferedImage image) { if (STANDARD_DRP.equals(dprY) && STANDARD_DRP.equals(dprX)) { return image; } int scaledWidth = (int) (image.getWidth() / dprX); int scaledHeight = (int) (image.getHeight() / dprY); final BufferedImage bufferedImage = new BufferedImage(scaledWidth, scaledHeight, image.getType()); final Graphics2D graphics2D = bufferedImage.createGraphics(); graphics2D.setComposite(AlphaComposite.Src); graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics2D.drawImage(image, 0, 0, scaledWidth, scaledHeight, null); graphics2D.dispose(); return bufferedImage; } } ================================================ FILE: src/main/java/pazone/ashot/Screenshot.java ================================================ package pazone.ashot; import pazone.ashot.coordinates.Coords; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * @author Pavel Zorin * @author Maksim Mukosey * * Result of screen capture. * Contains final processed image and all required information for image comparison. */ public class Screenshot implements Serializable { private static final long serialVersionUID = 1241241256734156872L; private transient BufferedImage image; private Set ignoredAreas = new HashSet<>(); private Set coordsToCompare; /** * Coords, containing x and y shift from origin image coordinates system * Actually it is coordinates of cropped area on origin image. * Should be set if image is cropped. */ private Coords originShift = new Coords(0, 0); public BufferedImage getImage() { return image; } public void setImage(BufferedImage image) { this.image = image; } public Screenshot(BufferedImage image) { this.image = image; this.coordsToCompare = Collections.singleton(Coords.ofImage(image)); } public Set getCoordsToCompare() { return coordsToCompare; } public void setCoordsToCompare(Set coordsToCompare) { this.coordsToCompare = coordsToCompare; } public Set getIgnoredAreas() { return ignoredAreas; } public void setIgnoredAreas(Set ignoredAreas) { this.ignoredAreas = ignoredAreas; } public Coords getOriginShift() { return originShift; } public void setOriginShift(Coords originShift) { this.originShift = originShift; } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); ImageIO.write(image, "png", out); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); image = ImageIO.read(in); } } ================================================ FILE: src/main/java/pazone/ashot/ShootingDecorator.java ================================================ package pazone.ashot; import pazone.ashot.coordinates.Coords; import java.util.Set; public abstract class ShootingDecorator implements ShootingStrategy { private final ShootingStrategy shootingStrategy; protected ShootingDecorator(ShootingStrategy shootingStrategy) { this.shootingStrategy = shootingStrategy; } public ShootingStrategy getShootingStrategy() { return shootingStrategy; } /** * Default behavior is not to change coords, because by default coordinates are not changed */ @Override public Set prepareCoords(Set coordsSet) { return coordsSet; } } ================================================ FILE: src/main/java/pazone/ashot/ShootingStrategies.java ================================================ package pazone.ashot; import pazone.ashot.cutter.CutStrategy; import pazone.ashot.cutter.FixedCutStrategy; import pazone.ashot.cutter.VariableCutStrategy; /** * Utility class for different shooting strategies. */ public final class ShootingStrategies { private static final int SCROLL_TIMEOUT_IOS = 500; private static final int HEADER_IOS_7 = 98; private static final int HEADER_IOS_8_MIN = 41; private static final int HEADER_IOS_8_MAX = 65; private static final int VIEWPORT_MIN_IOS_8 = 960; private static final int VIEWPORT_MIN_IOS_8_SIM = 1250; private static final CutStrategy CUT_STRATEGY_IOS_7 = new FixedCutStrategy(HEADER_IOS_7, 0); private static final CutStrategy CUT_STRATEGY_IOS_8 = iOS8CutStrategy(VIEWPORT_MIN_IOS_8); private static final CutStrategy CUT_STRATEGY_IOS_8_SIM = iOS8CutStrategy(VIEWPORT_MIN_IOS_8_SIM); private ShootingStrategies() { throw new UnsupportedOperationException(); } /** * Simple shooting strategy. No image processing is performed. * @return new instance of SimpleShootingStrategy */ public static ShootingStrategy simple() { return new SimpleShootingStrategy(); } /** * Will scale down image according to dpr specified. * * @param shootingStrategy Shooting strategy used to take screenshots before scaling * @param dpr device pixel ratio * @return {@code ShootingStrategy} that will scale image according to {@code dpr} */ public static ShootingStrategy scaling(ShootingStrategy shootingStrategy, float dpr) { return new ScalingDecorator(shootingStrategy).withDpr(dpr); } /** * Will scale down image according to dpr specified. * * @param dpr device pixel ratio * @return {@code ShootingStrategy} that will scale image according to {@code dpr} */ public static ShootingStrategy scaling(float dpr) { return scaling(simple(), dpr); } /** * Will cut header and footer off from screen shot. * * @param shootingStrategy Shooting strategy used to take screenshots before cutting * @param cutStrategy {@link CutStrategy} to use. See {@link FixedCutStrategy} or {@link VariableCutStrategy} * @return {@code ShootingStrategy} with custom cutting strategy */ public static ShootingStrategy cutting(ShootingStrategy shootingStrategy, CutStrategy cutStrategy) { return new CuttingDecorator(shootingStrategy).withCutStrategy(cutStrategy); } /** * Will cut header and footer off from screen shot. * * @param cutStrategy {@link CutStrategy} to use. See {@link FixedCutStrategy} or {@link VariableCutStrategy} * @return {@code ShootingStrategy} with custom cutting strategy */ public static ShootingStrategy cutting(CutStrategy cutStrategy) { return cutting(simple(), cutStrategy); } /** * Will cut header and footer off from screen shot. * * @param headerToCut header to cut in pixels * @param footerToCut footer to cut in pixels * @return {@code ShootingStrategy} with {@link FixedCutStrategy} cutting strategy */ public static ShootingStrategy cutting(int headerToCut, int footerToCut) { return cutting(new FixedCutStrategy(headerToCut, footerToCut)); } /** * Will scroll viewport while shooting. * * @param shootingStrategy Shooting strategy used to take screenshots between scrolls * @param scrollTimeout time between viewportPasting scrolls in milliseconds * @return {@code ShootingStrategy} with custom timeout between scrolls */ public static ShootingStrategy viewportPasting(ShootingStrategy shootingStrategy, int scrollTimeout) { return new ViewportPastingDecorator(shootingStrategy).withScrollTimeout(scrollTimeout); } /** * Will scroll viewport while shooting. * * @param scrollTimeout time between viewportPasting scrolls in milliseconds * @return {@code ShootingStrategy} with custom timeout between scrolls */ public static ShootingStrategy viewportPasting(int scrollTimeout) { return viewportPasting(simple(), scrollTimeout); } /** * Will scroll viewport while shooting and cut off browser's header and footer * * @param shootingStrategy Underneath shooting strategy * @param scrollTimeout time between scrolls in milliseconds * @param cutStrategy strategy to cut header and footer from image * * @return {@code ShootingStrategy} with custom scroll timeout and cutting strategy */ public static ShootingStrategy viewportNonRetina(ShootingStrategy shootingStrategy, int scrollTimeout, CutStrategy cutStrategy) { return viewportPasting(cutting(shootingStrategy, cutStrategy), scrollTimeout); } /** * Will scroll viewport while shooting and cut off browser's header and footer * * @param scrollTimeout time between scrolls in milliseconds * @param cutStrategy strategy to cut header and footer from image * * @return {@code ShootingStrategy} with custom scroll timeout and cutting strategy */ public static ShootingStrategy viewportNonRetina(int scrollTimeout, CutStrategy cutStrategy) { return viewportPasting(cutting(cutStrategy), scrollTimeout); } /** * Will scroll viewportPasting while shooting and cut off browser's header and footer * * @param scrollTimeout time between scrolls in milliseconds * @param headerToCut height of header to cut from image * @param footerToCut height of footer to cut from image * * @return {@code ShootingStrategy} with custom scroll timeout and header/footer to cut */ public static ShootingStrategy viewportNonRetina(int scrollTimeout, int headerToCut, int footerToCut) { return viewportNonRetina(scrollTimeout, new FixedCutStrategy(headerToCut, footerToCut)); } /** * Will scale screenshots and scroll viewportPasting while shooting and cut off browser's header/footer * * @param shootingStrategy Underneath shooting strategy * @param scrollTimeout time between scrolls in milliseconds * @param cutStrategy strategy to cut header and footer from image * @param dpr device pixel ratio * * @return {@code ShootingStrategy} with custom DPR scaling and scroll timeout and cutting strategy */ public static ShootingStrategy viewportRetina(ShootingStrategy shootingStrategy, int scrollTimeout, CutStrategy cutStrategy, float dpr) { ShootingStrategy scalingDecorator = scaling(shootingStrategy, dpr); return viewportNonRetina(scalingDecorator, scrollTimeout, cutStrategy); } /** * Will scale screenshots and scroll viewportPasting while shooting and cut off browser's header/footer * * @param scrollTimeout time between scrolls in milliseconds * @param cutStrategy strategy to cut header and footer from image * @param dpr device pixel ratio * * @return {@code ShootingStrategy} with custom DPR scaling and scroll timeout and cutting strategy */ public static ShootingStrategy viewportRetina(int scrollTimeout, CutStrategy cutStrategy, float dpr) { return viewportRetina(simple(), scrollTimeout, cutStrategy, dpr); } /** * Will scale screenshots and scroll viewportPasting while shooting and cut off browser's header/footer * * @param scrollTimeout time between scrolls in milliseconds * @param headerToCut height of header to cut from image * @param footerToCut height of footer to cut from image * @param dpr device pixel ratio * * @return {@code ShootingStrategy} with custom DPR scaling and scroll timeout and cutting strategy */ public static ShootingStrategy viewportRetina(int scrollTimeout, int headerToCut, int footerToCut, float dpr) { return viewportRetina(scrollTimeout, new FixedCutStrategy(headerToCut, footerToCut), dpr); } /** * Will scale screenshots and scroll viewportPasting while shooting and cut off browser's header/footer * * @param shootingStrategy Underneath shooting strategy * @param scrollTimeout time between scrolls in milliseconds * @param headerToCut height of header to cut from image * @param footerToCut height of footer to cut from image * @param dpr device pixel ratio * * @return {@code ShootingStrategy} with custom DPR scaling and scroll timeout and cutting strategy */ public static ShootingStrategy viewportRetina(ShootingStrategy shootingStrategy, int scrollTimeout, int headerToCut, int footerToCut, float dpr) { return viewportRetina(shootingStrategy, scrollTimeout, new FixedCutStrategy(headerToCut, footerToCut), dpr); } public static ShootingStrategy iPad2WithIOS7(ShootingStrategy shootingStrategy) { return viewportIOSNonRetina(shootingStrategy, CUT_STRATEGY_IOS_7); } public static ShootingStrategy iPad2WithIOS7() { return iPad2WithIOS7(simple()); } public static ShootingStrategy iPad2WithIOS8(ShootingStrategy shootingStrategy) { return viewportIOSNonRetina(shootingStrategy, CUT_STRATEGY_IOS_8); } public static ShootingStrategy iPad2WithIOS8() { return iPad2WithIOS8(simple()); } public static ShootingStrategy iPad2WithIOS8Simulator(ShootingStrategy shootingStrategy) { return viewportIOSNonRetina(shootingStrategy, CUT_STRATEGY_IOS_8_SIM); } public static ShootingStrategy iPad2WithIOS8Simulator() { return iPad2WithIOS8Simulator(simple()); } public static ShootingStrategy iPad2WithIOS8Retina(ShootingStrategy shootingStrategy) { return viewportIOSRetina(shootingStrategy, CUT_STRATEGY_IOS_8); } public static ShootingStrategy iPad2WithIOS8Retina() { return iPad2WithIOS8Retina(simple()); } public static ShootingStrategy iPad2WithIOS8RetinaSimulator(ShootingStrategy shootingStrategy) { return viewportIOSRetina(shootingStrategy, CUT_STRATEGY_IOS_8_SIM); } public static ShootingStrategy iPad2WithIOS8RetinaSimulator() { return iPad2WithIOS8RetinaSimulator(simple()); } /** * Will create screenshot's of the whole page and rotate landscape images * * @param scrollTimeout time between scrolls in milliseconds * @param cutStrategy strategy to cut header and footer from image * @return {@code ShootingStrategy} which will shoot whole page and rotate landscape images */ public static ShootingStrategy iPadLandscapeOrientation(int scrollTimeout, CutStrategy cutStrategy) { return viewportPasting(iPadLandscapeOrientationSimple(cutStrategy), scrollTimeout); } /** * Will rotate screenshot's made on iPad in landscape orientation * * @param cutStrategy strategy to cut header and footer from image * @return {@code ShootingStrategy} which will rotate image */ public static ShootingStrategy iPadLandscapeOrientationSimple(CutStrategy cutStrategy) { return new RotatingDecorator(cutStrategy, simple()); } private static ShootingStrategy viewportIOSNonRetina(ShootingStrategy shootingStrategy, CutStrategy cutStrategy) { return viewportNonRetina(shootingStrategy, SCROLL_TIMEOUT_IOS, cutStrategy); } private static ShootingStrategy viewportIOSRetina(ShootingStrategy shootingStrategy, CutStrategy cutStrategy) { return viewportRetina(shootingStrategy, SCROLL_TIMEOUT_IOS, cutStrategy, 2F); } private static CutStrategy iOS8CutStrategy(int minViewport) { return new VariableCutStrategy(HEADER_IOS_8_MIN, HEADER_IOS_8_MAX, minViewport); } } ================================================ FILE: src/main/java/pazone/ashot/ShootingStrategy.java ================================================ package pazone.ashot; import org.openqa.selenium.WebDriver; import pazone.ashot.coordinates.Coords; import java.awt.image.BufferedImage; import java.io.Serializable; import java.util.Set; /** * @author Pavel Zorin */ public interface ShootingStrategy extends Serializable { /** * Get's screenshot of whole page or viewport (depends on browser) * * @param wd WebDrvier * @return image of the whole page or viewport */ BufferedImage getScreenshot(WebDriver wd); /** * Get's screenshot of area or areas that are defined by {@link Coords} * * @param wd WebDriver * @param coords Set of coordinates to shoot * @return minimal image with required coords */ BufferedImage getScreenshot(WebDriver wd, Set coords); /** * Prepares coordinated for cropper and ignored areas * * @param coordsSet to prepare * @return New set of prepared coordinates */ Set prepareCoords(Set coordsSet); } ================================================ FILE: src/main/java/pazone/ashot/SimpleShootingStrategy.java ================================================ package pazone.ashot; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.Augmenter; import pazone.ashot.coordinates.Coords; import pazone.ashot.util.ImageTool; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Set; /** * Gets screenshot from webdriver. */ public class SimpleShootingStrategy implements ShootingStrategy { @Override public BufferedImage getScreenshot(WebDriver wd) { TakesScreenshot takesScreenshot; try { takesScreenshot = (TakesScreenshot) wd; } catch (ClassCastException ignored) { takesScreenshot = (TakesScreenshot) new Augmenter().augment(wd); } try { byte[] imageBytes = takesScreenshot.getScreenshotAs(OutputType.BYTES); return ImageTool.toBufferedImage(imageBytes); } catch (IOException e) { throw new ImageReadException("Can not parse screenshot data", e); } } @Override public BufferedImage getScreenshot(WebDriver wd, Set coords) { return getScreenshot(wd); } /** * Default behavior is not to change coords, because by default coordinates are not changed */ @Override public Set prepareCoords(Set coordsSet) { return coordsSet; } } ================================================ FILE: src/main/java/pazone/ashot/ViewportPastingDecorator.java ================================================ package pazone.ashot; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import pazone.ashot.coordinates.Coords; import pazone.ashot.util.InnerScript; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Will scroll viewport and shoot to get an image of full page. * Useful for browsers on portable devices. * @author Pavel Zorin */ public class ViewportPastingDecorator extends ShootingDecorator { public static final String PAGE_DIMENSIONS_JS = "js/page_dimensions.js"; protected int scrollTimeout = 0; private Coords shootingArea; public ViewportPastingDecorator(ShootingStrategy strategy) { super(strategy); } public ViewportPastingDecorator withScrollTimeout(int scrollTimeout) { this.scrollTimeout = scrollTimeout; return this; } @Override public BufferedImage getScreenshot(WebDriver wd) { return getScreenshot(wd, null); } @Override public BufferedImage getScreenshot(WebDriver wd, Set coordsSet) { JavascriptExecutor js = (JavascriptExecutor) wd; int initialY = getCurrentScrollY(js); try { PageDimensions pageDimensions = getPageDimensions(wd); shootingArea = getShootingCoords(coordsSet, pageDimensions); BufferedImage finalImage = new BufferedImage(pageDimensions.getViewportWidth(), shootingArea.height, BufferedImage.TYPE_3BYTE_BGR); Graphics2D graphics = finalImage.createGraphics(); int viewportHeight = pageDimensions.getViewportHeight(); int scrollTimes = (int) Math.ceil(shootingArea.getHeight() / viewportHeight); for (int n = 0; n < scrollTimes; n++) { scrollVertically(js, shootingArea.y + viewportHeight * n); waitForScrolling(); BufferedImage part = getChunk(wd, n, scrollTimes); graphics.drawImage(part, 0, getCurrentScrollY(js) - shootingArea.y, null); } graphics.dispose(); return finalImage; } finally { scrollVertically(js, initialY); } } @Override public Set prepareCoords(Set coordsSet) { return shootingArea == null ? coordsSet : shiftCoords(coordsSet, shootingArea); } protected PageDimensions getPageDimensions(WebDriver driver) { Map pageDimensions = InnerScript.execute(PAGE_DIMENSIONS_JS, driver); return new PageDimensions(pageDimensions.get("pageHeight").intValue(), pageDimensions.get("viewportWidth").intValue(), pageDimensions.get("viewportHeight").intValue()); } protected int getCurrentScrollY(JavascriptExecutor js) { return ((Number) js.executeScript("var scrY = window.pageYOffset;" + "if(scrY){return scrY;} else {return 0;}")).intValue(); } protected void scrollVertically(JavascriptExecutor js, int scrollY) { js.executeScript("scrollTo(0, arguments[0]); return [];", scrollY); } /** * Returns a single chunk (viewport screenshot) used to build a full screenshot * * @param wd WebDriver instance * @param currentChunkIndex Zero-based index of the chunk to get * @param totalNumberOfChunks Total number of chunks * @return Current screenshot chunk (viewport screenshot) */ protected BufferedImage getChunk(WebDriver wd, int currentChunkIndex, int totalNumberOfChunks) { return getShootingStrategy().getScreenshot(wd); } private Coords getShootingCoords(Set coords, PageDimensions pageDimensions) { if (coords == null || coords.isEmpty()) { return new Coords(0, 0, pageDimensions.getViewportWidth(), pageDimensions.getPageHeight()); } return extendShootingArea(Coords.unity(coords), pageDimensions); } private Set shiftCoords(Set coordsSet, Coords shootingArea) { Set shiftedCoords = new HashSet<>(); if (coordsSet != null) { for (Coords coords : coordsSet) { coords.y -= shootingArea.y; shiftedCoords.add(coords); } } return shiftedCoords; } private Coords extendShootingArea(Coords shootingCoords, PageDimensions pageDimensions) { int halfViewport = pageDimensions.getViewportHeight() / 2; shootingCoords.y = Math.max(shootingCoords.y - halfViewport / 2, 0); shootingCoords.height = Math.min(shootingCoords.height + halfViewport, pageDimensions.getPageHeight()); return shootingCoords; } private void waitForScrolling() { try { Thread.sleep(scrollTimeout); } catch (InterruptedException e) { throw new IllegalStateException("Exception while waiting for scrolling", e); } } } ================================================ FILE: src/main/java/pazone/ashot/comparison/DiffMarkupPolicy.java ================================================ package pazone.ashot.comparison; import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import static java.awt.image.BufferedImage.TYPE_BYTE_INDEXED; /** * @author Rovniakov Viacheslav rovner@yandex-team.ru */ public abstract class DiffMarkupPolicy { private static final int BITS_PER_PIXEL = 8; private static final int COLOR_MAP_SIZE = 2; private static final int TRANSPARENT_COLOR_INDEX = 0; protected boolean marked = false; protected int diffSizeTrigger; protected BufferedImage diffImage; protected Color diffColor = Color.RED; public DiffMarkupPolicy withDiffColor(final Color diffColor) { this.diffColor = diffColor; return this; } public abstract BufferedImage getMarkedImage(); public abstract BufferedImage getTransparentMarkedImage(); public abstract void addDiffPoint(int x, int y); @Override public abstract boolean equals(Object obj); @Override public abstract int hashCode(); public abstract boolean hasDiff(); public abstract int getDiffSize(); public void setDiffImage(BufferedImage diffImage) { this.diffImage = diffImage; } public void setDiffSizeTrigger(final int diffSizeTrigger) { this.diffSizeTrigger = diffSizeTrigger; } public BufferedImage getDiffImage() { return diffImage; } private IndexColorModel getColorModel() { return new IndexColorModel(BITS_PER_PIXEL, COLOR_MAP_SIZE, getColorMap(), 0, false, TRANSPARENT_COLOR_INDEX); } private byte[] getColorMap() { Color negativeColor = new Color(0xFFFFFF - diffColor.getRGB()); //negate diff color return new byte[]{ (byte) negativeColor.getRed(), (byte) negativeColor.getGreen(), (byte) negativeColor.getBlue(), (byte) diffColor.getRed(), (byte) diffColor.getGreen(), (byte) diffColor.getBlue() }; } protected BufferedImage getTransparentDiffImage(BufferedImage diffImage) { return new BufferedImage(diffImage.getWidth(), diffImage.getHeight(), TYPE_BYTE_INDEXED, getColorModel()); } } ================================================ FILE: src/main/java/pazone/ashot/comparison/ImageDiff.java ================================================ package pazone.ashot.comparison; import java.awt.image.BufferedImage; /** * @author Pavel Zorin */ public class ImageDiff { @SuppressWarnings("UnusedDeclaration") public static final ImageDiff EMPTY_DIFF = new ImageDiff(); private final DiffMarkupPolicy diffMarkupPolicy; public ImageDiff(DiffMarkupPolicy diffMarkupPolicy) { this.diffMarkupPolicy = diffMarkupPolicy; } private ImageDiff() { diffMarkupPolicy = new PointsMarkupPolicy(); } /** * Sets the maximum number of distinguished pixels when images are still considered the same. * * @param diffSizeTrigger the number of different pixels * @return self for fluent style */ public ImageDiff withDiffSizeTrigger(final int diffSizeTrigger) { this.diffMarkupPolicy.setDiffSizeTrigger(diffSizeTrigger); return this; } /** * @return Diff image with empty spaces in diff areas. */ public BufferedImage getDiffImage() { return diffMarkupPolicy.getDiffImage(); } /** * Sets Diff image. * @param image Image diff */ public void setDiffImage(BufferedImage image) { diffMarkupPolicy.setDiffImage(image); } public void addDiffPoint(int x, int y) { diffMarkupPolicy.addDiffPoint(x, y); } /** * Marks diff on inner image and returns it. * Idempotent. * * @return marked diff image */ public BufferedImage getMarkedImage() { return diffMarkupPolicy.getMarkedImage(); } /** * Marks diff points on transparent canvas and returns it. * Idempotent. * * @return marked diff image */ public BufferedImage getTransparentMarkedImage() { return diffMarkupPolicy.getTransparentMarkedImage(); } /** * Returns true if there are differences between images. * * @return true if there are differences between images. */ public boolean hasDiff() { return diffMarkupPolicy.hasDiff(); } /** * Returns number of points that differ. * * @return int - number of points that differ. */ public int getDiffSize() { return diffMarkupPolicy.getDiffSize(); } @Override public boolean equals(Object obj) { if (obj instanceof ImageDiff) { ImageDiff item = (ImageDiff) obj; return this.diffMarkupPolicy.equals(item.diffMarkupPolicy); } return false; } @Override public int hashCode() { return this.diffMarkupPolicy.hashCode(); } } ================================================ FILE: src/main/java/pazone/ashot/comparison/ImageDiffer.java ================================================ package pazone.ashot.comparison; import pazone.ashot.Screenshot; import pazone.ashot.coordinates.Coords; import pazone.ashot.util.ImageBytesDiffer; import java.awt.Color; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.util.LinkedHashSet; import java.util.Set; import static pazone.ashot.util.ImageTool.rgbCompare; /** * @author Pavel Zorin */ public class ImageDiffer { private static final int DEFAULT_COLOR_DISTORTION = 15; private int colorDistortion = DEFAULT_COLOR_DISTORTION; private DiffMarkupPolicy diffMarkupPolicy = new PointsMarkupPolicy(); private Color ignoredColor = null; public ImageDiffer withIgnoredColor(final Color ignoreColor) { this.ignoredColor = ignoreColor; return this; } public ImageDiffer withColorDistortion(int distortion) { this.colorDistortion = distortion; return this; } /** * Sets the diff markup policy. * * @param diffMarkupPolicy diff markup policy instance * @return self for fluent style * @see ImageMarkupPolicy * @see PointsMarkupPolicy */ public ImageDiffer withDiffMarkupPolicy(final DiffMarkupPolicy diffMarkupPolicy) { this.diffMarkupPolicy = diffMarkupPolicy; return this; } public ImageDiff makeDiff(Screenshot expected, Screenshot actual) { ImageDiff diff = new ImageDiff(diffMarkupPolicy); if (ImageBytesDiffer.areImagesEqual(expected, actual)) { diff.setDiffImage(actual.getImage()); } else { markDiffPoints(expected, actual, diff); } return diff; } protected void markDiffPoints(Screenshot expected, Screenshot actual, ImageDiff diff) { Coords expectedImageCoords = Coords.ofImage(expected.getImage()); Coords actualImageCoords = Coords.ofImage(actual.getImage()); CoordsSet compareCoordsSet = new CoordsSet( CoordsSet.union(actual.getCoordsToCompare(), expected.getCoordsToCompare())); CoordsSet ignoreCoordsSet = new CoordsSet( CoordsSet.intersection(actual.getIgnoredAreas(), expected.getIgnoredAreas())); int width = Math.max(expected.getImage().getWidth(), actual.getImage().getWidth()); int height = Math.max(expected.getImage().getHeight(), actual.getImage().getHeight()); diff.setDiffImage(createDiffImage(expected.getImage(), actual.getImage(), width, height)); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (ignoreCoordsSet.contains(i, j)) { continue; } if (!isInsideBothImages(i, j, expectedImageCoords, actualImageCoords) || compareCoordsSet.contains(i, j) && hasDiffInChannel(expected, actual, i, j)) { diff.addDiffPoint(i, j); } } } } private boolean hasDiffInChannel(Screenshot expected, Screenshot actual, int i, int j) { if (ignoredColor != null && rgbCompare(expected.getImage().getRGB(i, j), ignoredColor.getRGB(), 0)) { return false; } return !rgbCompare(expected.getImage().getRGB(i, j), actual.getImage().getRGB(i, j), colorDistortion); } public ImageDiff makeDiff(BufferedImage expected, BufferedImage actual) { return makeDiff(new Screenshot(expected), new Screenshot(actual)); } private BufferedImage createDiffImage(BufferedImage expectedImage, BufferedImage actualImage, int width, int height) { BufferedImage diffImage = new BufferedImage(width, height, actualImage.getType()); paintImage(actualImage, diffImage); paintImage(expectedImage, diffImage); return diffImage; } private void paintImage(BufferedImage image, BufferedImage diffImage) { Graphics graphics = diffImage.getGraphics(); graphics.drawImage(image, 0, 0, null); graphics.dispose(); } private boolean isInsideBothImages(int i, int j, Coords expected, Coords actual) { return expected.contains(i, j) && actual.contains(i, j); } private static class CoordsSet { private final boolean isSingle; private final Coords minRectangle; private final Set coordsSet; CoordsSet(Set coordsSet) { isSingle = coordsSet.size() == 1; this.coordsSet = coordsSet; int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; int maxX = 0; int maxY = 0; for (Coords coords : coordsSet) { minX = Math.min(minX, (int) coords.getMinX()); minY = Math.min(minY, (int) coords.getMinY()); maxX = Math.max(maxX, (int) coords.getMaxX()); maxY = Math.max(maxY, (int) coords.getMaxY()); } minRectangle = new Coords(minX, minY, maxX - minX, maxY - minY); } private boolean contains(int i, int j) { return inaccurateContains(i, j) && accurateContains(i, j); } private boolean inaccurateContains(int i, int j) { return minRectangle.contains(i, j); } private boolean accurateContains(int i, int j) { return isSingle || coordsSet.stream().anyMatch(coords -> coords.contains(i, j)); } private static Set intersection(Set coordsPool1, Set coordsPool2) { return Coords.intersection(coordsPool1, coordsPool2); } private static Set union(Set coordsPool1, Set coordsPool2) { Set coordsPool = new LinkedHashSet<>(); coordsPool.addAll(coordsPool1); coordsPool.addAll(coordsPool2); return coordsPool; } } } ================================================ FILE: src/main/java/pazone/ashot/comparison/ImageMarkupPolicy.java ================================================ package pazone.ashot.comparison; import java.awt.image.BufferedImage; /** * @author Rovniakov Viacheslav rovner@yandex-team.ru * */ public class ImageMarkupPolicy extends DiffMarkupPolicy { private int diffPointCount; private int xReference = Integer.MAX_VALUE; private int yReference = Integer.MAX_VALUE; private int xSum; private int ySum; private BufferedImage transparentDiffImage; @Override public void setDiffImage(BufferedImage diffImage) { super.setDiffImage(diffImage); transparentDiffImage = getTransparentDiffImage(diffImage); } @Override public BufferedImage getMarkedImage() { if (!marked) { final int rgb = diffColor.getRGB(); for (int x = 0; x < diffImage.getWidth(); x++) { for (int y = 0; y < diffImage.getHeight(); y++) { if (transparentDiffImage.getRGB(x, y) == rgb) { diffImage.setRGB(x, y, rgb); } } } marked = true; } return diffImage; } @Override public BufferedImage getTransparentMarkedImage() { return transparentDiffImage; } @Override public void addDiffPoint(int x, int y) { diffPointCount++; xReference = Math.min(xReference, x); yReference = Math.min(yReference, y); xSum += x; ySum += y; transparentDiffImage.setRGB(x, y, diffColor.getRGB()); } @Override public boolean equals(Object obj) { if (obj instanceof ImageMarkupPolicy) { ImageMarkupPolicy item = (ImageMarkupPolicy) obj; return this.diffPointCount == item.diffPointCount && this.xSum - this.diffPointCount * this.xReference == item.xSum - item.diffPointCount * item.xReference && this.ySum - this.diffPointCount * this.yReference == item.ySum - item.diffPointCount * item.yReference; } return false; } @Override public int hashCode() { int result = diffPointCount; result = 31 * result + xSum - diffPointCount * xReference; result = 31 * result + ySum - diffPointCount * yReference; return result; } @Override public boolean hasDiff() { return diffPointCount > diffSizeTrigger; } @Override public int getDiffSize() { return diffPointCount; } } ================================================ FILE: src/main/java/pazone/ashot/comparison/PointsMarkupPolicy.java ================================================ package pazone.ashot.comparison; import java.awt.Point; import java.awt.image.BufferedImage; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; /** * @author Rovniakov Viacheslav rovner@yandex-team.ru * */ public class PointsMarkupPolicy extends DiffMarkupPolicy { private final Set diffPoints = new LinkedHashSet<>(); private Set deposedPoints = new LinkedHashSet<>(); private BufferedImage transparentMarkedImage = null; @Override public BufferedImage getMarkedImage() { if (!marked) { markDiffPoints(diffImage); marked = true; } return diffImage; } @Override public BufferedImage getTransparentMarkedImage() { if (transparentMarkedImage == null) { transparentMarkedImage = getTransparentDiffImage(diffImage); markDiffPoints(transparentMarkedImage); } return transparentMarkedImage; } @Override public void addDiffPoint(int x, int y) { diffPoints.add(new Point(x, y)); } @Override public boolean equals(Object obj) { if (obj instanceof PointsMarkupPolicy) { PointsMarkupPolicy item = (PointsMarkupPolicy) obj; return diffPoints.size() == item.diffPoints.size() && item.getDeposedPoints().containsAll( getDeposedPoints()); } return false; } @Override public int hashCode() { return getDeposedPoints().hashCode(); } @Override public boolean hasDiff() { return diffPoints.size() > diffSizeTrigger; } @Override public int getDiffSize() { return diffPoints.size(); } protected void markDiffPoints(BufferedImage image) { int rgb = diffColor.getRGB(); for (Point dot : diffPoints) { image.setRGB(dot.x, dot.y, rgb); } } private Set getDeposedPoints() { if (deposedPoints.isEmpty()) { deposedPoints = deposeReference(); } return deposedPoints; } private Point getReferenceCorner() { Iterator iterator = diffPoints.iterator(); Point diffPoint = iterator.next(); double x = diffPoint.getX(); double y = diffPoint.getY(); while (iterator.hasNext()) { diffPoint = iterator.next(); x = Math.min(x, diffPoint.getX()); y = Math.min(y, diffPoint.getY()); } return new Point((int) x, (int) y); } private Set deposeReference() { Point reference = getReferenceCorner(); Set referenced = new HashSet<>(); for (Point point : diffPoints) { referenced.add(new Point(point.x - reference.x, point.y - reference.y)); } return referenced; } } ================================================ FILE: src/main/java/pazone/ashot/coordinates/Coords.java ================================================ package pazone.ashot.coordinates; import com.google.gson.Gson; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.util.Collection; import java.util.HashSet; import java.util.Set; /** * @author Pavel Zorin */ public class Coords extends Rectangle { public static Set intersection(Collection coordsPool1, Collection coordsPool2) { Set intersectedCoords = new HashSet<>(); for (Coords coords1 : coordsPool1) { for (Coords coords2 : coordsPool2) { Coords intersection = coords1.intersection(coords2); if (!intersection.isEmpty()) { intersectedCoords.add(intersection); } } } return intersectedCoords; } public static Set setReferenceCoords(Coords reference, Set coordsSet) { Set referencedCoords = new HashSet<>(); for (Coords coords : coordsSet) { referencedCoords.add(new Coords( coords.x - reference.x, coords.y - reference.y, coords.width, coords.height) ); } return referencedCoords; } public static Coords unity(Collection coordsCollection) { Coords unity = coordsCollection.iterator().next(); for (Coords coords : coordsCollection) { unity = unity.union(coords); } return unity; } public static Coords ofImage(BufferedImage image) { return new Coords(image.getWidth(), image.getHeight()); } public Coords(Rectangle rectangle) { super(rectangle); } public Coords(int x, int y, int width, int height) { super(x, y, width, height); } public Coords(int width, int height) { super(width, height); } public void reduceBy(int pixels) { if (pixels < getWidth() / 2 && pixels < getHeight() / 2) { this.x += pixels; this.y += pixels; this.width -= pixels; this.height -= pixels; } } @SuppressWarnings("NullableProblems") @Override public Coords union(Rectangle r) { return new Coords(super.union(r)); } @SuppressWarnings("NullableProblems") @Override public Coords intersection(Rectangle r) { return new Coords(super.intersection(r)); } @Override public String toString() { return new Gson().toJson(this); } } ================================================ FILE: src/main/java/pazone/ashot/coordinates/CoordsPreparationStrategy.java ================================================ package pazone.ashot.coordinates; import pazone.ashot.Screenshot; import java.util.Collection; import java.util.HashSet; import java.util.Set; import static pazone.ashot.coordinates.Coords.intersection; import static pazone.ashot.coordinates.Coords.setReferenceCoords; /** * @author Pavel Zorin */ public abstract class CoordsPreparationStrategy { public static CoordsPreparationStrategy simple() { return new CoordsPreparationStrategy() { @Override public Set prepare(Collection coordinates) { return new HashSet<>(coordinates); } }; } public static CoordsPreparationStrategy intersectingWith(final Screenshot screenshot) { return new CoordsPreparationStrategy() { @Override public Set prepare(Collection coordinates) { return intersection(screenshot.getCoordsToCompare(), setReferenceCoords(screenshot.getOriginShift(), new HashSet<>(coordinates))); } }; } public abstract Set prepare(Collection coordinates); } ================================================ FILE: src/main/java/pazone/ashot/coordinates/CoordsProvider.java ================================================ package pazone.ashot.coordinates; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * @author Pavel Zorin */ public abstract class CoordsProvider implements Serializable { public abstract Coords ofElement(WebDriver driver, WebElement element); public Set ofElements(WebDriver driver, Iterable elements) { Set elementsCoords = new HashSet<>(); for (WebElement element : elements) { Coords elementCoords = ofElement(driver, element); if (!elementCoords.isEmpty()) { elementsCoords.add(elementCoords); } } return Collections.unmodifiableSet(elementsCoords); } @SuppressWarnings("UnusedDeclaration") public Set ofElements(WebDriver driver, WebElement... elements) { return ofElements(driver, Arrays.asList(elements)); } @SuppressWarnings("UnusedDeclaration") public Set locatedBy(WebDriver driver, By locator) { return ofElements(driver, driver.findElements(locator)); } } ================================================ FILE: src/main/java/pazone/ashot/coordinates/JqueryCoordsProvider.java ================================================ package pazone.ashot.coordinates; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import pazone.ashot.util.JsCoords; /** * @author pazone */ public class JqueryCoordsProvider extends CoordsProvider { @Override public Coords ofElement(WebDriver driver, WebElement element) { return JsCoords.findCoordsWithJquery(driver, element); } } ================================================ FILE: src/main/java/pazone/ashot/coordinates/WebDriverCoordsProvider.java ================================================ package pazone.ashot.coordinates; import org.openqa.selenium.Dimension; import org.openqa.selenium.Point; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; /** * @author Pavel Zorin */ public class WebDriverCoordsProvider extends CoordsProvider { @Override public Coords ofElement(WebDriver driver, WebElement element) { // Keep Point/Dimension as opposed to Rectangle because browsers like PhantomJS do not like the latter Point point = element.getLocation(); Dimension dimension = element.getSize(); return new Coords( point.getX(), point.getY(), dimension.getWidth(), dimension.getHeight()); } } ================================================ FILE: src/main/java/pazone/ashot/cropper/DefaultCropper.java ================================================ package pazone.ashot.cropper; import pazone.ashot.Screenshot; import pazone.ashot.coordinates.Coords; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.util.Set; import static pazone.ashot.coordinates.Coords.setReferenceCoords; /** * @author Pavel Zorin */ public class DefaultCropper extends ImageCropper { @Override public Screenshot cropScreenshot(BufferedImage image, Set coordsToCompare) { Coords cropArea = Coords.unity(coordsToCompare); Coords imageIntersection = Coords.ofImage(image).intersection(cropArea); if (imageIntersection.isEmpty()) { return new Screenshot(image); } BufferedImage cropped = new BufferedImage(imageIntersection.width, imageIntersection.height, image.getType()); Graphics g = cropped.getGraphics(); g.drawImage( image, 0, 0, imageIntersection.width, imageIntersection.height, cropArea.x, cropArea.y, cropArea.x + imageIntersection.width, cropArea.y + imageIntersection.height, null ); g.dispose(); Screenshot screenshot = new Screenshot(cropped); screenshot.setOriginShift(cropArea); screenshot.setCoordsToCompare(setReferenceCoords(screenshot.getOriginShift(), coordsToCompare)); return screenshot; } protected Coords createCropArea(Set coordsToCompare) { return Coords.unity(coordsToCompare); } } ================================================ FILE: src/main/java/pazone/ashot/cropper/ImageCropper.java ================================================ package pazone.ashot.cropper; import pazone.ashot.Screenshot; import pazone.ashot.coordinates.Coords; import java.awt.image.BufferedImage; import java.io.Serializable; import java.util.Set; /** * @author Pavel Zorin */ public abstract class ImageCropper implements Serializable { public Screenshot crop(BufferedImage image, Set cropArea) { return cropArea.isEmpty() ? new Screenshot(image) : cropScreenshot(image, cropArea); } protected abstract Screenshot cropScreenshot(BufferedImage image, Set coordsToCompare); } ================================================ FILE: src/main/java/pazone/ashot/cropper/indent/BlurFilter.java ================================================ package pazone.ashot.cropper.indent; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; /** * @author Pavel Zorin */ public class BlurFilter implements IndentFilter { @Override public BufferedImage apply(BufferedImage image) { Kernel kernel = new Kernel(3, 3, new float[] { 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f } ); BufferedImageOp blurOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null); return blurOp.filter(image, null); } } ================================================ FILE: src/main/java/pazone/ashot/cropper/indent/IndentCropper.java ================================================ package pazone.ashot.cropper.indent; import pazone.ashot.Screenshot; import pazone.ashot.cropper.DefaultCropper; import pazone.ashot.coordinates.Coords; import pazone.ashot.util.ImageTool; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Set; import static pazone.ashot.coordinates.Coords.setReferenceCoords; /** * @author Pavel Zorin */ public class IndentCropper extends DefaultCropper { public static final int DEFAULT_INDENT = 50; private int indent; protected final List filters = new LinkedList<>(); public IndentCropper(final int indent) { this.indent = indent; } public IndentCropper() { this(DEFAULT_INDENT); } @Override public Screenshot cropScreenshot(BufferedImage image, Set coordsToCompare) { Coords cropArea = createCropArea(coordsToCompare); Coords indentMask = createIndentMask(cropArea, image); Coords coordsWithIndent = applyIndentMask(cropArea, indentMask); Screenshot croppedShot = super.cropScreenshot(image, Collections.singleton(coordsWithIndent)); croppedShot.setOriginShift(coordsWithIndent); croppedShot.setCoordsToCompare(setReferenceCoords(coordsWithIndent, coordsToCompare)); List noFilteringAreas = createNotFilteringAreas(croppedShot); croppedShot.setImage(applyFilters(croppedShot.getImage())); pasteAreasToCompare(croppedShot.getImage(), noFilteringAreas); return croppedShot; } protected Coords applyIndentMask(Coords origin, Coords mask) { Coords spreadCoords = new Coords(0, 0); spreadCoords.x = origin.x - mask.x; spreadCoords.y = origin.y - mask.y; spreadCoords.height = mask.y + origin.height + mask.height; spreadCoords.width = mask.x + origin.width + mask.width; return spreadCoords; } protected Coords createIndentMask(Coords originCoords, BufferedImage image) { Coords indentMask = new Coords(originCoords); indentMask.x = Math.min(indent, originCoords.x); indentMask.y = Math.min(indent, originCoords.y); indentMask.width = Math.min(indent, image.getWidth() - originCoords.x - originCoords.width); indentMask.height = Math.min(indent, image.getHeight() - originCoords.y - originCoords.height); return indentMask; } protected List createNotFilteringAreas(Screenshot screenshot) { List noFilteringAreas = new ArrayList<>(); for (Coords noFilteringCoords : screenshot.getCoordsToCompare()) { if (noFilteringCoords.intersects(Coords.ofImage(screenshot.getImage()))) { noFilteringAreas.add(new NoFilteringArea(screenshot.getImage(), noFilteringCoords)); } } return noFilteringAreas; } protected void pasteAreasToCompare(BufferedImage filtered, List noFilteringAreas) { Graphics graphics = filtered.getGraphics(); for (NoFilteringArea noFilteringArea : noFilteringAreas) { graphics.drawImage( noFilteringArea.getSubimage(), noFilteringArea.getCoords().x, noFilteringArea.getCoords().y, null); } graphics.dispose(); } public IndentCropper addIndentFilter(IndentFilter filter) { this.filters.add(filter); return this; } protected BufferedImage applyFilters(BufferedImage image) { for (IndentFilter filter : filters) { image = filter.apply(image); } return image; } private static final class NoFilteringArea { private final BufferedImage subimage; private final Coords coords; private NoFilteringArea(BufferedImage origin, Coords noFilterCoords) { this.subimage = ImageTool.subImage(origin, noFilterCoords); this.coords = noFilterCoords; } public BufferedImage getSubimage() { return subimage; } public Coords getCoords() { return coords; } } } ================================================ FILE: src/main/java/pazone/ashot/cropper/indent/IndentFilerFactory.java ================================================ package pazone.ashot.cropper.indent; /** * @author Pavel Zorin */ public final class IndentFilerFactory { private IndentFilerFactory() { throw new UnsupportedOperationException(); } public static BlurFilter blur() { return new BlurFilter(); } public static MonochromeFilter monochrome() { return new MonochromeFilter(); } } ================================================ FILE: src/main/java/pazone/ashot/cropper/indent/IndentFilter.java ================================================ package pazone.ashot.cropper.indent; import java.awt.image.BufferedImage; import java.io.Serializable; /** * @author Pavel Zorin */ public interface IndentFilter extends Serializable { BufferedImage apply(BufferedImage image); } ================================================ FILE: src/main/java/pazone/ashot/cropper/indent/MonochromeFilter.java ================================================ package pazone.ashot.cropper.indent; import pazone.ashot.util.ImageTool; import java.awt.image.BufferedImage; import javax.swing.GrayFilter; /** * @author Pavel Zorin */ public class MonochromeFilter implements IndentFilter { @Override public BufferedImage apply(BufferedImage image) { return darken(image); } private BufferedImage darken(BufferedImage image) { return ImageTool.toBufferedImage(GrayFilter.createDisabledImage(image)); } } ================================================ FILE: src/main/java/pazone/ashot/cutter/CutStrategy.java ================================================ package pazone.ashot.cutter; import org.openqa.selenium.WebDriver; import java.io.Serializable; /** * @author Vyacheslav Frolov */ public interface CutStrategy extends Serializable { /** * Obtains height of header that will be cut off from initial screenshot. * @param driver - webDriver * @return height of header in pixels */ int getHeaderHeight(WebDriver driver); /** * Obtains height of footer that will be cut off from initial screenshot. * @param driver - webDriver * @return height of header in pixels */ int getFooterHeight(WebDriver driver); /** * Obtains width of left bar that will be cut off from initial screenshot. * * @param driver - webDriver * @return width of the left bar in pixels */ default int getLeftBarWidth(WebDriver driver) { return 0; } /** * Obtains width of right bar that will be cut off from initial screenshot. * * @param driver - webDriver * @return width of the right bar in pixels */ default int getRightBarWidth(WebDriver driver) { return 0; } } ================================================ FILE: src/main/java/pazone/ashot/cutter/FixedCutStrategy.java ================================================ package pazone.ashot.cutter; import org.openqa.selenium.WebDriver; /** * Strategy for cutting header and footer of a constant height. * * @author Vyacheslav Frolov */ public class FixedCutStrategy implements CutStrategy { private final int headerToCut; private final int footerToCut; private final int leftBarToCut; private final int rightBarToCut; public FixedCutStrategy(int headerToCut, int footerToCut) { this(headerToCut, footerToCut, 0, 0); } public FixedCutStrategy(int headerToCut, int footerToCut, int leftBarToCut, int rightBarToCut) { this.headerToCut = headerToCut; this.footerToCut = footerToCut; this.leftBarToCut = leftBarToCut; this.rightBarToCut = rightBarToCut; } @Override public int getHeaderHeight(WebDriver driver) { return headerToCut; } @Override public int getFooterHeight(WebDriver driver) { return footerToCut; } @Override public int getLeftBarWidth(WebDriver driver) { return leftBarToCut; } @Override public int getRightBarWidth(WebDriver driver) { return rightBarToCut; } } ================================================ FILE: src/main/java/pazone/ashot/cutter/VariableCutStrategy.java ================================================ package pazone.ashot.cutter; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import pazone.ashot.InvalidViewportHeightException; /** * Strategy for cutting header and footer with variable height.
* For example, Safari browser in iOS 8 introduced a feature (so called 'minimal-ui')
* when browser's header might be 65px or 41px (with address bar hidden).
* This strategy will get current height of browser's header and footer. * @author Vyacheslav Frolov */ public class VariableCutStrategy implements CutStrategy { public static final String SCRIPT = "var h = window.innerHeight || document.documentElement.clientHeight; " + "return h;"; private final int headerMin; private final int headerMax; private final int windowInnerHeightMin; private final int footerMax; private final int footerMin; /** * @param headerMin - minimal header height (for Safari iOS 8 it is 41px) * @param headerMax - maximum header height (for Safari iOS 8 it is 65px) * @param footerMin - minimal footer height (for Safari iOS 8 it is 0px) * @param footerMax - maximum footer height (for Safari iOS 8 it is 89px) * @param windowInnerHeightMin - minimal height of viewportPasting (when header with address bar shown).
* For real device iPad 2 it is 960px (portrait) and 674px (landscape).
* For simulated iPad 2 it is 1225px (portrait) and 687px (landscape). */ public VariableCutStrategy(int headerMin, int headerMax, int footerMin, int footerMax, int windowInnerHeightMin) { this.headerMin = headerMin; this.headerMax = headerMax; this.footerMin = footerMin; this.footerMax = footerMax; this.windowInnerHeightMin = windowInnerHeightMin; } public VariableCutStrategy(int headerMin, int headerMax, int footerMax, int windowInnerHeightMin) { this(headerMin, headerMax, 0, footerMax, windowInnerHeightMin); } public VariableCutStrategy(int headerMin, int headerMax, int windowInnerHeightMin) { this(headerMin, headerMax, 0, windowInnerHeightMin); } @Override public int getHeaderHeight(WebDriver driver) { return getCutHeight((JavascriptExecutor) driver, headerMin, headerMax); } @Override public int getFooterHeight(WebDriver driver) { if (0 == footerMax && 0 == footerMin) { return 0; } return getCutHeight((JavascriptExecutor) driver, footerMin, footerMax); } private int getCutHeight(JavascriptExecutor driver, int heightMin, int heightMax) { final int innerHeight = getWindowInnerHeight(driver); return innerHeight > windowInnerHeightMin ? heightMin : heightMax; } private int getWindowInnerHeight(JavascriptExecutor driver) { final Number innerHeight; try { innerHeight = (Number) driver.executeScript(SCRIPT); } catch (ClassCastException e) { throw new InvalidViewportHeightException("Could not acquire window.innerHeight property!", e); } if (innerHeight == null) { throw new InvalidViewportHeightException( "Could not acquire window.innerHeight property! Returned value is null."); } return innerHeight.intValue(); } } ================================================ FILE: src/main/java/pazone/ashot/util/ImageBytesDiffer.java ================================================ package pazone.ashot.util; import pazone.ashot.Screenshot; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; /** * @author Viacheslav Frolov */ public final class ImageBytesDiffer { private ImageBytesDiffer() { throw new UnsupportedOperationException(); } public static boolean areImagesEqual(Screenshot expected, Screenshot actual) { return areImagesEqual(expected.getImage(), actual.getImage()); } public static boolean areImagesEqual(BufferedImage expected, BufferedImage actual) { return expected.getHeight() == actual.getHeight() && expected.getWidth() == actual.getWidth() && actual.getColorModel().equals(expected.getColorModel()) && areImagesBuffersEqual(expected.getRaster().getDataBuffer(), actual.getRaster().getDataBuffer()); } private static boolean areImagesBuffersEqual(DataBuffer expected, DataBuffer actual) { return actual.getDataType() == expected.getDataType() && actual.getNumBanks() == expected.getNumBanks() && actual.getSize() == expected.getSize() && areImagesBytesEqual(actual, expected); } private static boolean areImagesBytesEqual(DataBuffer expected, DataBuffer actual) { for (int bank = 0; bank < expected.getNumBanks(); bank++) { for (int i = 0; i < expected.getSize(); i++) { if (expected.getElem(bank, i) != actual.getElem(bank, i)) { return false; } } } return true; } } ================================================ FILE: src/main/java/pazone/ashot/util/ImageTool.java ================================================ package pazone.ashot.util; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import pazone.ashot.Screenshot; import pazone.ashot.coordinates.Coords; import javax.imageio.ImageIO; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; /** * @author Pavel Zorin */ public final class ImageTool { private ImageTool() { throw new UnsupportedOperationException(); } public static BufferedImage subImage(BufferedImage origin, Coords crop) { Coords intersection = Coords.ofImage(origin).intersection(crop); return origin.getSubimage(intersection.x, intersection.y, intersection.width, intersection.height); } public static Coords spreadCoordsInsideImage(Coords coordinates, int indent, BufferedImage image) { return new Coords(Math.max(0, coordinates.x - indent), Math.max(0, coordinates.y - indent), Math.min(image.getWidth(), coordinates.width + indent), Math.min(image.getHeight(), coordinates.height + indent)); } public static boolean rgbCompare(int rgb1, int rgb2, int inaccuracy) { if (inaccuracy == 0) { return rgb1 == rgb2; } int red1 = (rgb1 & 0x00FF0000) >> 16; int green1 = (rgb1 & 0x0000FF00) >> 8; int blue1 = (rgb1 & 0x000000FF); int red2 = (rgb2 & 0x00FF0000) >> 16; int green2 = (rgb2 & 0x0000FF00) >> 8; int blue2 = (rgb2 & 0x000000FF); return Math.abs(red1 - red2) <= inaccuracy && Math.abs(green1 - green2) <= inaccuracy && Math.abs(blue1 - blue2) <= inaccuracy; } public static Matcher equalImage(final BufferedImage second) { return new TypeSafeMatcher() { @Override protected boolean matchesSafely(BufferedImage first) { if (!Coords.ofImage(first).equals(Coords.ofImage(second))) { return false; } for (int x = 0; x < first.getWidth(); x++) { for (int y = 0; y < first.getHeight(); y++) { if (!rgbCompare(first.getRGB(x, y), second.getRGB(x, y), 10)) { return false; } } } return true; } @Override public void describeTo(Description description) { } }; } @SuppressWarnings("UnusedDeclaration") public static byte[] toByteArray(Screenshot screenshot) throws IOException { return toByteArray(screenshot.getImage()); } public static byte[] toByteArray(BufferedImage image) throws IOException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { ImageIO.write(image, "png", baos); return baos.toByteArray(); } } public static BufferedImage toBufferedImage(Image img) { if (img instanceof BufferedImage) { return (BufferedImage) img; } BufferedImage bufferedImage = new BufferedImage( img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB ); Graphics2D graphics = bufferedImage.createGraphics(); graphics.drawImage(img, 0, 0, null); graphics.dispose(); return bufferedImage; } public static BufferedImage toBufferedImage(byte[] imageBytes) throws IOException { try (ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes)) { return ImageIO.read(bais); } } } ================================================ FILE: src/main/java/pazone/ashot/util/InnerScript.java ================================================ package pazone.ashot.util; import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import static java.lang.Thread.currentThread; /** * @author Pavel Zorin */ public final class InnerScript { private InnerScript() { throw new UnsupportedOperationException(); } public static T execute(String path, WebDriver driver, Object... args) { try { String script = IOUtils.toString(currentThread().getContextClassLoader().getResourceAsStream(path), StandardCharsets.UTF_8); //noinspection unchecked return (T) ((JavascriptExecutor) driver).executeScript(script, args); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: src/main/java/pazone/ashot/util/JsCoords.java ================================================ package pazone.ashot.util; import java.util.List; import com.google.gson.Gson; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import pazone.ashot.coordinates.Coords; /** * @author Pavel Zorin */ public final class JsCoords { public static final String COORDS_JS_PATH = "js/coords-single.js"; private JsCoords() { throw new UnsupportedOperationException(); } public static Coords findCoordsWithJquery(WebDriver driver, WebElement element) { List result = InnerScript.execute(COORDS_JS_PATH, driver, element); if (result.isEmpty()) { throw new RuntimeException("Unable to find coordinates with jQuery."); } return new Gson().fromJson((String) result.get(0), Coords.class); } } ================================================ FILE: src/main/resources/js/coords-single.js ================================================ function Coords(el) { this.left = parseInt(el.offset().left); this.top = parseInt(el.offset().top); this.right = parseInt(this.left + el.outerWidth()); this.bottom = parseInt(this.top + el.outerHeight()); } Coords.prototype.toString = function () { var x = Math.max(this.left, 0); var y = Math.max(this.top, 0); return JSON.stringify({ x:x, y:y, width:this.right - x, height:this.bottom - y }); }; return [(new Coords($(arguments[0]))).toString()]; ================================================ FILE: src/main/resources/js/page_dimensions.js ================================================ var body = document.body; var documentElement = document.documentElement; var pageHeight = Math.max(body.scrollHeight, body.offsetHeight, documentElement.clientHeight, documentElement.scrollHeight, documentElement.offsetHeight); var viewportHeight = window.innerHeight || documentElement.clientHeight|| body.clientHeight; var viewportWidth = window.innerWidth || documentElement.clientWidth || body.clientWidth; return {"pageHeight": pageHeight, "viewportHeight": viewportHeight, "viewportWidth": viewportWidth} ================================================ FILE: src/test/java/pazone/ashot/CdpShootingStrategyTest.java ================================================ package pazone.ashot; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Base64; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chromium.HasCdp; import pazone.ashot.coordinates.Coords; import pazone.ashot.util.ImageTool; import pazone.ashot.util.TestImageUtils; @ExtendWith(MockitoExtension.class) class CdpShootingStrategyTest { private final ShootingStrategy strategy = new CdpShootingStrategy(); @Mock(extraInterfaces = HasCdp.class) private WebDriver webDriver; @Test void testPageScreenshot() throws IOException { BufferedImage expected = TestImageUtils.IMAGE_A_SMALL; String base = Base64.getEncoder().encodeToString(ImageTool.toByteArray(expected)); when(((HasCdp) webDriver).executeCdpCommand("Page.captureScreenshot", Map.of("captureBeyondViewport", true))) .thenReturn(Map.of("data", base)); BufferedImage actual = strategy.getScreenshot(webDriver); TestImageUtils.assertImageEquals(actual, expected); } @Test void testElementScreenshot() throws IOException { BufferedImage expected = TestImageUtils.IMAGE_A_SMALL; String base = Base64.getEncoder().encodeToString(ImageTool.toByteArray(expected)); Coords coords = new Coords(1, 2, 3, 4); when(((HasCdp) webDriver).executeCdpCommand("Page.captureScreenshot", Map.of("captureBeyondViewport", true, "clip", Map.of("x", coords.x, "y", coords.y, "width", coords.width, "height", coords.height, "scale", 1)))) .thenReturn(Map.of("data", base)); BufferedImage actual = strategy.getScreenshot(webDriver, Set.of(coords)); TestImageUtils.assertImageEquals(actual, expected); } @Test void testUnsupportedCdp() { WebDriver driver = mock(WebDriver.class); IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> strategy.getScreenshot(driver)); assertEquals("WebDriver instance must support Chrome DevTools protocol", thrown.getMessage()); } } ================================================ FILE: src/test/java/pazone/ashot/CroppersTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import pazone.ashot.coordinates.Coords; import pazone.ashot.cropper.DefaultCropper; import pazone.ashot.cropper.ImageCropper; import pazone.ashot.cropper.indent.IndentCropper; import pazone.ashot.cropper.indent.IndentFilerFactory; import java.util.Collections; import java.util.Set; import java.util.stream.Stream; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL; import static pazone.ashot.util.TestImageUtils.assertImageEquals; /** * @author Pavel Zorin */ class CroppersTest { private static final Set OUTSIDE_IMAGE = Collections.singleton(new Coords(20, 20, 200, 90)); private static final Set INSIDE_IMAGE = Collections.singleton(new Coords(20, 20, 30, 30)); static Stream outsideCropperData() { return Stream.of( Arguments.of(new DefaultCropper(), "img/expected/outside_dc.png"), Arguments.of(new IndentCropper(10), "img/expected/outside_ic.png") ); } @ParameterizedTest @MethodSource("outsideCropperData") void testElementOutsideImageCropper(ImageCropper cropper, String expectedImagePath) { Screenshot screenshot = cropper.crop(IMAGE_A_SMALL, OUTSIDE_IMAGE); assertImageEquals(screenshot.getImage(), expectedImagePath); } @Test void testElementInsideImageIndentCropperWithFilter() { Screenshot screenshot = new IndentCropper() .addIndentFilter(IndentFilerFactory.blur()) .addIndentFilter(IndentFilerFactory.monochrome()) .cropScreenshot(IMAGE_A_SMALL, INSIDE_IMAGE); assertImageEquals(screenshot.getImage(), "img/expected/inside_icf.png"); } } ================================================ FILE: src/test/java/pazone/ashot/CuttingDecoratorTest.java ================================================ package pazone.ashot; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static pazone.ashot.util.TestImageUtils.assertImageEquals; import java.util.function.UnaryOperator; import org.junit.jupiter.api.Test; import pazone.ashot.cutter.FixedCutStrategy; import pazone.ashot.util.TestImageUtils; class CuttingDecoratorTest { public static final String CUT_FROM_ALL_THE_SIDES_PNG = "cut-from-all-the-sides.png"; @Test void shouldCutFromAllTheSides() { testCuttingFromAllTheSides(cd -> cd.withCut(35, 35, 40, 40), CUT_FROM_ALL_THE_SIDES_PNG); } @Test void shouldCutFromAllTheSidesUsingFixedCutStrategy() { testCuttingFromAllTheSides(cd -> cd.withCutStrategy(new FixedCutStrategy(35, 35, 40, 40)), CUT_FROM_ALL_THE_SIDES_PNG); } @Test void shouldCutOnlyFooterAndHeader() { testCuttingFromAllTheSides(cd -> cd.withCut(35, 35), "cut-footer-header.png"); } private void testCuttingFromAllTheSides(UnaryOperator settings, String expectedImageName) { ShootingStrategy shootingStrategy = mock(ShootingStrategy.class); CuttingDecorator cuttingDecorator = new CuttingDecorator(shootingStrategy); when(shootingStrategy.getScreenshot(null)).thenReturn(TestImageUtils.IMAGE_B_SMALL); assertImageEquals(settings.apply(cuttingDecorator).getScreenshot(null), "img/expected/" + expectedImageName); } } ================================================ FILE: src/test/java/pazone/ashot/DiffMarkupPolicyTest.java ================================================ package pazone.ashot; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import pazone.ashot.comparison.DiffMarkupPolicy; import pazone.ashot.comparison.ImageMarkupPolicy; import pazone.ashot.comparison.PointsMarkupPolicy; import java.awt.Point; import java.util.HashSet; import java.util.Set; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL; import static pazone.ashot.util.TestImageUtils.IMAGE_B_SMALL; /** * @author Rovniakov Viacheslav rovner@yandex-team.ru */ class DiffMarkupPolicyTest { private static Stream data() { return Stream.of( Arguments.of(new PointsMarkupPolicy(), new PointsMarkupPolicy()), Arguments.of(new ImageMarkupPolicy(), new ImageMarkupPolicy()) ); } private void initDiffMarkupPolicies(DiffMarkupPolicy diffMarkupPolicyA, DiffMarkupPolicy diffMarkupPolicyB) { diffMarkupPolicyA.setDiffImage(IMAGE_A_SMALL); diffMarkupPolicyB.setDiffImage(IMAGE_B_SMALL); } @ParameterizedTest @MethodSource("data") void testEquality(DiffMarkupPolicy diffMarkupPolicyA, DiffMarkupPolicy diffMarkupPolicyB) { initDiffMarkupPolicies(diffMarkupPolicyA, diffMarkupPolicyB); addDiffPoints(getDiffPointsA(), diffMarkupPolicyA, 1, 2); addDiffPoints(getDiffPointsA(), diffMarkupPolicyB, 0, 3); assertThat(diffMarkupPolicyA, equalTo(diffMarkupPolicyB)); assertThat(diffMarkupPolicyA.hashCode(), equalTo(diffMarkupPolicyB.hashCode())); } @ParameterizedTest @MethodSource("data") void testNotEquality(DiffMarkupPolicy diffMarkupPolicyA, DiffMarkupPolicy diffMarkupPolicyB) { initDiffMarkupPolicies(diffMarkupPolicyA, diffMarkupPolicyB); addDiffPoints(getDiffPointsA(), diffMarkupPolicyA, 0, 0); addDiffPoints(getDiffPointsB(), diffMarkupPolicyB, 0, 0); assertThat(diffMarkupPolicyA, not(equalTo(diffMarkupPolicyB))); assertThat(diffMarkupPolicyA.hashCode(), not(equalTo(diffMarkupPolicyB.hashCode()))); } @ParameterizedTest @MethodSource("data") void testNotEqualityByNumber(DiffMarkupPolicy diffMarkupPolicyA, DiffMarkupPolicy diffMarkupPolicyB) { initDiffMarkupPolicies(diffMarkupPolicyA, diffMarkupPolicyB); addDiffPoints(getDiffPointsA(), diffMarkupPolicyA, 0, 0); addDiffPoints(getDiffPointsB(), diffMarkupPolicyA, 0, 0); diffMarkupPolicyB.addDiffPoint(0, 0); assertThat(diffMarkupPolicyA, not(equalTo(diffMarkupPolicyB))); assertThat(diffMarkupPolicyA.hashCode(), not(equalTo(diffMarkupPolicyB.hashCode()))); } private Set getDiffPointsA() { return new HashSet() {{ add(new Point(3, 4)); add(new Point(3, 5)); add(new Point(3, 6)); add(new Point(4, 4)); add(new Point(4, 5)); add(new Point(4, 6)); }}; } private Set getDiffPointsB() { return new HashSet() {{ add(new Point(3, 4)); add(new Point(3, 5)); add(new Point(3, 6)); add(new Point(4, 4)); add(new Point(4, 5)); add(new Point(4, 7)); }}; } private void addDiffPoints(Set points, DiffMarkupPolicy diffMarkupPolicy, int xShift, int yShift) { for (Point point : points) { diffMarkupPolicy.addDiffPoint((int) point.getX() - xShift, (int) point.getY() - yShift); } } } ================================================ FILE: src/test/java/pazone/ashot/DifferTest.java ================================================ package pazone.ashot; import pazone.ashot.comparison.DiffMarkupPolicy; import pazone.ashot.comparison.ImageDiff; import pazone.ashot.comparison.ImageDiffer; import pazone.ashot.comparison.ImageMarkupPolicy; import pazone.ashot.comparison.PointsMarkupPolicy; import pazone.ashot.coordinates.Coords; import java.awt.Color; import java.awt.image.BufferedImage; import java.util.Collections; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL_PATH; import static pazone.ashot.util.TestImageUtils.IMAGE_B_SMALL; import static pazone.ashot.util.TestImageUtils.assertImageEquals; import static pazone.ashot.util.TestImageUtils.loadImage; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; /** * @author Pavel Zorin */ class DifferTest { private static final BufferedImage IMAGE_B_BIG = loadImage("img/B_b.png"); private static final String IMAGE_IGNORED_TEMPLATE = "img/ignore_color_template.png"; private static final String IMAGE_IGNORED_PASS = "img/ignore_color_pass.png"; private static final String IMAGE_IGNORED_FAIL = "img/ignore_color_fail.png"; private static Stream data() { return Stream.of(new PointsMarkupPolicy(), new ImageMarkupPolicy()); } private ImageDiffer createImageDiffer(DiffMarkupPolicy diffMarkupPolicy) { return new ImageDiffer() .withColorDistortion(10) .withDiffMarkupPolicy(diffMarkupPolicy.withDiffColor(Color.RED)); } @ParameterizedTest @MethodSource("data") void testSameSizeDiff(DiffMarkupPolicy diffMarkupPolicy) { ImageDiff diff = createImageDiffer(diffMarkupPolicy).makeDiff(IMAGE_A_SMALL, IMAGE_B_SMALL); assertAll( () -> assertImageEquals(diff.getMarkedImage(), "img/expected/same_size_diff.png"), () -> assertImageEquals(diff.getTransparentMarkedImage(), "img/expected/transparent_diff.png"), () -> assertThat(diff.withDiffSizeTrigger(624).hasDiff(), is(false)), () -> assertThat(diff.withDiffSizeTrigger(623).hasDiff(), is(true)) ); } @ParameterizedTest @MethodSource("data") void testDifferentSizeDiff(DiffMarkupPolicy diffMarkupPolicy) { ImageDiff diff = createImageDiffer(diffMarkupPolicy).makeDiff(IMAGE_B_SMALL, IMAGE_B_BIG); assertImageEquals(diff.getMarkedImage(), "img/expected/different_size_diff.png"); } @ParameterizedTest @MethodSource("data") void testSetDiffColor(DiffMarkupPolicy diffMarkupPolicy) { ImageDiffer greenDiffer = new ImageDiffer() .withDiffMarkupPolicy(diffMarkupPolicy.withDiffColor(Color.GREEN)); ImageDiff diff = greenDiffer.makeDiff(IMAGE_A_SMALL, IMAGE_B_SMALL); assertImageEquals(diff.getMarkedImage(), "img/expected/green_diff.png"); } @ParameterizedTest @MethodSource("data") void testEqualImagesDiff(DiffMarkupPolicy diffMarkupPolicy) { ImageDiff diff = createImageDiffer(diffMarkupPolicy).makeDiff(IMAGE_A_SMALL, IMAGE_A_SMALL); assertFalse(diff.hasDiff()); } static Stream dataWithIgnoredColorDiff() { return Stream.of( Arguments.of(Color.MAGENTA, IMAGE_IGNORED_PASS, false), Arguments.of(Color.MAGENTA, IMAGE_IGNORED_FAIL, true), Arguments.of(Color.RED, IMAGE_IGNORED_PASS, true) ); } @ParameterizedTest @MethodSource("dataWithIgnoredColorDiff") void testDiffImagesWithIgnoredColorDiff(Color ignoredColor, String imageToCompare, boolean hasDiff) { ImageDiffer imageDifferWithIgnored = new ImageDiffer().withIgnoredColor(ignoredColor); ImageDiff diff = imageDifferWithIgnored.makeDiff(loadImage(IMAGE_IGNORED_TEMPLATE), loadImage(imageToCompare)); assertThat(diff.hasDiff(), equalTo(hasDiff)); } @ParameterizedTest @MethodSource("data") void testIgnoredCoordsSame(DiffMarkupPolicy diffMarkupPolicy) { Screenshot a = createScreenshotWithIgnoredAreas(IMAGE_A_SMALL, new Coords(50, 50)); Screenshot b = createScreenshotWithIgnoredAreas(IMAGE_B_SMALL, new Coords(50, 50)); ImageDiff diff = createImageDiffer(diffMarkupPolicy).makeDiff(a, b); assertImageEquals(diff.getMarkedImage(), "img/expected/ignore_coords_same.png"); } @ParameterizedTest @MethodSource("data") void testIgnoredCoordsNotSame(DiffMarkupPolicy diffMarkupPolicy) { Screenshot a = createScreenshotWithIgnoredAreas(IMAGE_A_SMALL, new Coords(55, 55)); Screenshot b = createScreenshotWithIgnoredAreas(IMAGE_B_SMALL, new Coords(80, 80)); ImageDiff diff = createImageDiffer(diffMarkupPolicy).makeDiff(a, b); assertImageEquals(diff.getMarkedImage(), "img/expected/ignore_coords_not_same.png"); } @ParameterizedTest @MethodSource("data") void testCoordsToCompareAndIgnoredCombine(DiffMarkupPolicy diffMarkupPolicy) { Screenshot a = createScreenshotWithIgnoredAreas(IMAGE_A_SMALL, new Coords(60, 60)); a.setCoordsToCompare(Collections.singleton(new Coords(50, 50, 100, 100))); Screenshot b = createScreenshotWithIgnoredAreas(IMAGE_B_SMALL, new Coords(80, 80)); b.setCoordsToCompare(Collections.singleton(new Coords(50, 50, 100, 100))); ImageDiff diff = createImageDiffer(diffMarkupPolicy).makeDiff(a, b); assertImageEquals(diff.getMarkedImage(), "img/expected/combined_diff.png"); } @ParameterizedTest @MethodSource("data") void testDiffSize(DiffMarkupPolicy diffMarkupPolicy) { String path = IMAGE_A_SMALL_PATH; BufferedImage image1 = loadImage(path); BufferedImage image2 = loadImage(path); int rgb = Color.GREEN.getRGB(); int diffSize = 10; for (int i = 0; i < diffSize; i++) { image2.setRGB(i, 1, rgb); } ImageDiff imageDiff = createImageDiffer(diffMarkupPolicy).makeDiff(image1, image2); assertEquals(diffSize, imageDiff.getDiffSize(), "Should have diff size " + diffSize); } private Screenshot createScreenshotWithIgnoredAreas(BufferedImage image, Coords ignoredArea) { Screenshot screenshot = new Screenshot(image); screenshot.setIgnoredAreas(Collections.singleton(ignoredArea)); return screenshot; } } ================================================ FILE: src/test/java/pazone/ashot/ImageBytesDifferTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static pazone.ashot.util.ImageBytesDiffer.areImagesEqual; import static pazone.ashot.util.TestImageUtils.loadImage; import java.util.stream.Stream; /** * @author Viacheslav Frolov */ class ImageBytesDifferTest { @TestFactory Stream testDifferentImages() { return Stream.of( dynamicTest("Different size", () -> testDifferentImages("img/SolidColor_scaled.png")), dynamicTest("One pixel difference", () -> testDifferentImages("img/SolidColor_1px_red.png")), dynamicTest("Color mode difference", () -> testDifferentImages("img/SolidColor_indexed.png")) ); } void testDifferentImages(String path) { assertFalse(areImagesEqual(loadImage("img/SolidColor.png"), loadImage(path)), "Images should differ"); } @Test void testEqualImages() { assertTrue(areImagesEqual(loadImage("img/SolidColor.png"), loadImage("img/SolidColor.png")), "Images should equal"); } } ================================================ FILE: src/test/java/pazone/ashot/RotatingDecoratorTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import pazone.ashot.cutter.FixedCutStrategy; import pazone.ashot.util.ImageTool; import java.awt.image.BufferedImage; import java.io.IOException; import static org.mockito.Mockito.when; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL; import static pazone.ashot.util.TestImageUtils.assertImageEquals; /** * @author Rovniakov Viacheslav */ @ExtendWith(MockitoExtension.class) class RotatingDecoratorTest { @Mock(extraInterfaces = TakesScreenshot.class) private WebDriver webDriver; @Test void testRotating() throws IOException { when(((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES)).thenReturn( ImageTool.toByteArray(IMAGE_A_SMALL)); ShootingStrategy strategy = new RotatingDecorator(new FixedCutStrategy(0, 0), new SimpleShootingStrategy()); BufferedImage screenshot = strategy.getScreenshot(webDriver); assertImageEquals(screenshot, "img/expected/rotated.png"); } } ================================================ FILE: src/test/java/pazone/ashot/ScalingDecoratorTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import pazone.ashot.util.ImageTool; import pazone.ashot.util.TestImageUtils; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL; import static pazone.ashot.util.TestImageUtils.assertImageEquals; import java.awt.image.BufferedImage; import java.io.IOException; /** * @author Pavel Zorin */ @ExtendWith(MockitoExtension.class) class ScalingDecoratorTest { @Mock(extraInterfaces = TakesScreenshot.class) private WebDriver webDriver; @CsvSource({ "2, img/expected/dpr.png", "1, " + TestImageUtils.IMAGE_A_SMALL_PATH }) @ParameterizedTest void testDpr(float dpr, String expectedImagePath) throws IOException { when(((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES)).thenReturn( ImageTool.toByteArray(IMAGE_A_SMALL)); ShootingStrategy dpr2Strategy = new ScalingDecorator(new SimpleShootingStrategy()).withDpr(dpr); BufferedImage screenshot = dpr2Strategy.getScreenshot(webDriver); assertImageEquals(screenshot, expectedImagePath); assertEquals(IMAGE_A_SMALL.getType(), screenshot.getType()); } } ================================================ FILE: src/test/java/pazone/ashot/SerializeScreenshotTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import pazone.ashot.coordinates.Coords; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.file.Path; import java.util.Collections; import java.util.Set; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static pazone.ashot.util.TestImageUtils.IMAGE_A_SMALL; import static pazone.ashot.util.TestImageUtils.assertImageEquals; /** * @author Maksim Mukosey */ class SerializeScreenshotTest { static Stream> ignoredAreas() { return Stream.of( Collections.emptySet(), Collections.singleton(new Coords(20, 20, 200, 90)) ); } @ParameterizedTest @MethodSource("ignoredAreas") void testSerialization(Set ignoredAreas, @TempDir Path tempDir) throws IOException, ClassNotFoundException { File serializedFile = tempDir.resolve("serialized").toFile(); Screenshot screenshot = new Screenshot(IMAGE_A_SMALL); screenshot.setIgnoredAreas(ignoredAreas); try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(serializedFile))) { objectOutputStream.writeObject(screenshot); } try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(serializedFile))) { Screenshot deserialized = (Screenshot) objectInputStream.readObject(); assertThat(deserialized.getCoordsToCompare(), equalTo(screenshot.getCoordsToCompare())); assertThat(deserialized.getIgnoredAreas(), equalTo(screenshot.getIgnoredAreas())); assertImageEquals(deserialized.getImage(), screenshot.getImage()); } } } ================================================ FILE: src/test/java/pazone/ashot/VariableCutStrategyTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import pazone.ashot.cutter.CutStrategy; import pazone.ashot.cutter.VariableCutStrategy; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; /** * @author Vyacheslav Frolov */ @ExtendWith(MockitoExtension.class) class VariableCutStrategyTest { private static final int MAX_HEADER_HEIGHT = 65; private static final int MIN_HEADER_HEIGHT = 41; private static final int MIN_INNER_HEIGHT = 960; private final CutStrategy strategy = new VariableCutStrategy(MIN_HEADER_HEIGHT, MAX_HEADER_HEIGHT, MIN_INNER_HEIGHT); @Mock(extraInterfaces = JavascriptExecutor.class) private WebDriver webDriver; @ParameterizedTest @CsvSource({ "960, 65", "984, 41" }) void testGetBrowserHeaderHeight(long viewportHeight, int browserHeaderHeight) { mockViewportInnerHeight(viewportHeight); int headerHeight = strategy.getHeaderHeight(webDriver); assertThat("Header height should be detected correctly", browserHeaderHeight, is(headerHeight)); } @ParameterizedTest @CsvSource({ "a string", "," }) void testGetBrowserHeaderHeightWithInvalidViewportHeight(String viewportHeight) { mockViewportInnerHeight(viewportHeight); assertThrows(InvalidViewportHeightException.class, () -> strategy.getHeaderHeight(webDriver)); } private void mockViewportInnerHeight(Object viewportHeight) { when(((JavascriptExecutor) webDriver).executeScript(VariableCutStrategy.SCRIPT)).thenReturn(viewportHeight); } } ================================================ FILE: src/test/java/pazone/ashot/VerticalPastingShootingStrategyTest.java ================================================ package pazone.ashot; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import pazone.ashot.coordinates.Coords; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Set; import java.util.stream.Stream; import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR_PRE; import static java.util.Collections.singleton; import static org.hamcrest.CoreMatchers.everyItem; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class VerticalPastingShootingStrategyTest { private static final int VIEWPORT_HEIGHT = 80; private static final int DEFAULT_PAGE_HEIGHT = VIEWPORT_HEIGHT * 10 + VIEWPORT_HEIGHT / 2; private static final int PAGE_WIDTH = 100; private static final int DEFAULT_COORDS_INDENT = VIEWPORT_HEIGHT / 2; private final BufferedImage viewPortShot = new BufferedImage(PAGE_WIDTH, VIEWPORT_HEIGHT, TYPE_4BYTE_ABGR_PRE); private Coords shootingCoords; private BufferedImage screenshot; private Set preparedCoords; @Mock(extraInterfaces = {JavascriptExecutor.class, TakesScreenshot.class}) private WebDriver webDriver; @Spy private ViewportPastingDecorator shootingStrategy = new MockVerticalPastingShootingDecorator( new SimpleShootingStrategy()).withScrollTimeout(0); static Stream timesData() { return Stream.of( Arguments.of(VIEWPORT_HEIGHT, 2), Arguments.of(VIEWPORT_HEIGHT / 2, 1), Arguments.of(VIEWPORT_HEIGHT * 3, 4), Arguments.of(0, 1) ); } @ParameterizedTest @MethodSource("timesData") void testTimes(int height, int times) throws IOException { givenCoordsWithHeight(height); whenTakingScreenshot(shootingCoords); thenShootTimes(times); thenScreenshotIsHeight(height + DEFAULT_COORDS_INDENT); } @Test void testCoordsShiftWithDefaultIndent() throws IOException { givenCoordsWithHeight(VIEWPORT_HEIGHT / 3); whenTakingScreenshot(shootingCoords); whenPreparingCoords(singleton(shootingCoords)); thenCoordsShifted(); } @Test void testScreenshotFullPage() throws IOException { whenTakingScreenshot(); thenShootTimes(11); thenScreenshotIsHeight(DEFAULT_PAGE_HEIGHT); } private byte[] getImageAsBytes() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(viewPortShot, "PNG", baos); return baos.toByteArray(); } private void mockScreenshotting() throws IOException { when(((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES)).thenReturn(getImageAsBytes()); } private void givenCoordsWithHeight(int height) { shootingCoords = new Coords(0, VIEWPORT_HEIGHT * 4, PAGE_WIDTH, height); } private void whenTakingScreenshot() throws IOException { mockScreenshotting(); screenshot = shootingStrategy.getScreenshot(webDriver); } private void whenTakingScreenshot(Coords coords) throws IOException { mockScreenshotting(); screenshot = shootingStrategy.getScreenshot(webDriver, singleton(coords)); } private void whenPreparingCoords(Set coords) { preparedCoords = shootingStrategy.prepareCoords(coords); } private void thenCoordsShifted() { assertThat("Coords should be shifted correctly", preparedCoords, everyItem(hasProperty("y", is((double) DEFAULT_COORDS_INDENT / 2)))); } private void thenScreenshotIsHeight(int shotHeight) { assertThat("Screenshot height should be correct", screenshot.getHeight(), is(shotHeight)); } private void thenShootTimes(int times) { verify(((TakesScreenshot) webDriver), times(times)).getScreenshotAs(OutputType.BYTES); verify(shootingStrategy, times(times + 1)).scrollVertically(eq((JavascriptExecutor) webDriver), anyInt()); } static class MockVerticalPastingShootingDecorator extends ViewportPastingDecorator { MockVerticalPastingShootingDecorator(ShootingStrategy strategy) { super(strategy); } @Override protected PageDimensions getPageDimensions(WebDriver driver) { return new PageDimensions(DEFAULT_PAGE_HEIGHT, PAGE_WIDTH, VIEWPORT_HEIGHT); } @Override public int getCurrentScrollY(JavascriptExecutor js) { return 0; } } } ================================================ FILE: src/test/java/pazone/ashot/coordinates/WebDriverCoordsProviderTest.java ================================================ package pazone.ashot.coordinates; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import org.junit.jupiter.api.Test; import org.openqa.selenium.Dimension; import org.openqa.selenium.Point; import org.openqa.selenium.WebElement; class WebDriverCoordsProviderTest { private WebDriverCoordsProvider webDriverCoordsProvider = new WebDriverCoordsProvider(); @Test void testGetCoordinatesOfElement() { WebElement element = mock(WebElement.class); int x = 1; int y = 2; int height = 3; int width = 4; when(element.getLocation()).thenReturn(new Point(x, y)); when(element.getSize()).thenReturn(new Dimension(width, height)); Coords coords = webDriverCoordsProvider.ofElement(null, element); assertAll( () -> assertEquals(x, coords.x), () -> assertEquals(y, coords.y), () -> assertEquals(height, coords.height), () -> assertEquals(width, coords.width) ); } } ================================================ FILE: src/test/java/pazone/ashot/util/ImageToolTest.java ================================================ package pazone.ashot.util; import java.io.IOException; import org.junit.jupiter.api.Test; class ImageToolTest { @Test void shouldConvertImageToBytesAndViceVersa() throws IOException { byte[] imageBytes = ImageTool.toByteArray(TestImageUtils.IMAGE_A_SMALL); TestImageUtils.assertImageEquals(TestImageUtils.IMAGE_A_SMALL, ImageTool.toBufferedImage(imageBytes)); } } ================================================ FILE: src/test/java/pazone/ashot/util/TestImageUtils.java ================================================ package pazone.ashot.util; import static org.hamcrest.MatcherAssert.assertThat; import java.awt.image.BufferedImage; import java.io.IOException; import javax.imageio.ImageIO; public final class TestImageUtils { public static final String IMAGE_A_SMALL_PATH = "img/A_s.png"; public static final BufferedImage IMAGE_A_SMALL = loadImage(IMAGE_A_SMALL_PATH); public static final BufferedImage IMAGE_B_SMALL = loadImage("img/B_s.png"); private TestImageUtils() { } public static BufferedImage loadImage(String path) { try { return ImageIO.read(ClassLoader.getSystemResourceAsStream(path)); } catch (IOException e) { throw new RuntimeException(e); } } public static void assertImageEquals(BufferedImage actualImage, String expectedImagePath) { assertImageEquals(actualImage, loadImage(expectedImagePath)); } public static void assertImageEquals(BufferedImage actualImage, BufferedImage expectedImage) { assertThat(actualImage, ImageTool.equalImage(expectedImage)); } }