com.htmlhifive.pitalium.core.selenium.PtlWebDriver.java Source code

Java tutorial

Introduction

Here is the source code for com.htmlhifive.pitalium.core.selenium.PtlWebDriver.java

Source

/*
 * Copyright (C) 2015-2016 NS Solutions Corporation
 *
 * 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.
 */
package com.htmlhifive.pitalium.core.selenium;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.imageio.ImageIO;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.internal.JsonToWebElementConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.htmlhifive.pitalium.common.exception.TestRuntimeException;
import com.htmlhifive.pitalium.core.config.EnvironmentConfig;
import com.htmlhifive.pitalium.core.model.CompareTarget;
import com.htmlhifive.pitalium.core.model.DomSelector;
import com.htmlhifive.pitalium.core.model.IndexDomSelector;
import com.htmlhifive.pitalium.core.model.ScreenArea;
import com.htmlhifive.pitalium.core.model.ScreenAreaResult;
import com.htmlhifive.pitalium.core.model.ScreenAreaWrapper;
import com.htmlhifive.pitalium.core.model.ScreenshotArgument;
import com.htmlhifive.pitalium.core.model.ScreenshotParams;
import com.htmlhifive.pitalium.core.model.ScreenshotResult;
import com.htmlhifive.pitalium.core.model.SelectorType;
import com.htmlhifive.pitalium.core.model.TargetResult;
import com.htmlhifive.pitalium.image.model.RectangleArea;
import com.htmlhifive.pitalium.image.model.ScreenshotImage;
import com.htmlhifive.pitalium.image.util.ImageUtils;

/**
 * WebDriver?{@link org.openqa.selenium.remote.RemoteWebDriver}
 * ?????Web?????????????<br/>
 * ?????WebDriver?????????
 */
public abstract class PtlWebDriver extends RemoteWebDriver {

    /**
     * ?????1
     */
    public static final double DEFAULT_SCREENSHOT_SCALE = 1d;

    //@formatter:off
    // CHECKSTYLE:OFF
    private static final String[] SCRIPTS_SCROLL_TOP = { "document.documentElement.scrollTop",
            "document.body.scrollTop" };
    private static final String[] SCRIPTS_SCROLL_LEFT = { "document.documentElement.scrollLeft",
            "document.body.scrollLeft" };
    private static final String SCRIPT_GET_DEFAULT_DOCUMENT_OVERFLOW = "return {\"overflow\": document.documentElement.style.overflow};";
    private static final String SCRIPT_SET_DOCUMENT_OVERFLOW = "document.documentElement.style.overflow = arguments[0];";
    private static final String SCRIPT_GET_DEFAULT_BODY_STYLE = "return {"
            + "  \"position\":    document.body.style.position ? document.body.style.position : null,"
            + "  \"top\":         document.body.style.top      ? document.body.style.top      : null,"
            + "  \"left\":        document.body.style.left     ? document.body.style.left     : null,"
            + "  \"width\":       document.body.style.width    ? document.body.style.width    : null,"
            + "  \"scrollWidth\": document.body.scrollWidth};";

    private static final String SCRIPT_MOVE_BODY = "document.body.style.position = arguments[0];"
            + "document.body.style.top      = arguments[1];" + "document.body.style.left     = arguments[2];";
    private static final String GET_WINDOW_WIDTH_SCRIPT = "if (typeof window.innerWidth != 'undefined') {"
            + "  return window.innerWidth;"
            + "} else if (typeof document.documentElement != 'undefined' && typeof document.documentElement.clientWidth != 'undefined' && document.documentElement.clientWidth != 0) {"
            + "  return document.documentElement.clientWidth;" + "} else {"
            + "  return document.getElementsByTagName('body')[0].clientWidth;" + "}";
    private static final String GET_WINDOW_HEIGHT_SCRIPT = "if (typeof window.innerHeight != 'undefined') {"
            + "  return window.innerHeight;"
            + "} else if (typeof document.documentElement != 'undefined' && typeof document.documentElement.clientHeight != 'undefined' && document.documentElement.clientHeight != 0) {"
            + "  return document.documentElement.clientHeight;" + "} else {"
            + "  return document.getElementsByTagName('body')[0].clientHeight;" + "}";
    private static final String GET_SCROLL_WIDTH_SCRIPT = "return arguments[0].scrollWidth";
    private static final String GET_SCROLL_HEIGHT_SCRIPT = "return arguments[0].scrollHeight";
    private static final String GET_BODY_LEFT_SCRIPT = "var _bodyLeft = arguments[0].getBoundingClientRect().left; return _bodyLeft;";
    private static final String GET_BODY_TOP_SCRIPT = "var _bodyTop = arguments[0].getBoundingClientRect().top; return _bodyTop;";
    private static final long SCROLL_WAIT_MS = 100L;
    // CHECKSTYLE:ON
    //@formatter:on

    private static final Map<Capabilities, AtomicInteger> DEBUG_SCREENSHOT_COUNTS = new HashMap<Capabilities, AtomicInteger>();

    protected final Logger LOG = LoggerFactory.getLogger(getClass());
    private final PtlCapabilities capabilities;
    private String baseUrl;
    private double scale = DEFAULT_SCREENSHOT_SCALE;
    private EnvironmentConfig environmentConfig;

    /**
     * 
     * 
     * @param remoteAddress RemoteWebDriverServer?
     * @param capabilities Capability
     */
    protected PtlWebDriver(URL remoteAddress, PtlCapabilities capabilities) {
        super(remoteAddress, capabilities);
        this.capabilities = capabilities;

        // JsonToWebElementConverter??????findElement???RemoteWebElement??
        setElementConverter(new JsonToPtlWebElementConverter(this));
    }

    /**
     * ?URL????
     * 
     * @see com.htmlhifive.pitalium.core.config.TestAppConfig#baseUrl
     * @return ?URL
     */
    protected String getBaseUrl() {
        return baseUrl;
    }

    /**
     * ?URL???
     * 
     * @see com.htmlhifive.pitalium.core.config.TestAppConfig#baseUrl
     * @param baseUrl ?URL
     */
    protected void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    /**
     * ???
     * 
     * @param environmentConfig 
     */
    protected final void setEnvironmentConfig(EnvironmentConfig environmentConfig) {
        this.environmentConfig = environmentConfig;
    }

    /**
     * ??URL?????url?http://???https://???????????????{@link #baseUrl} + url?????
     * 
     * @param url ????URL
     */
    @Override
    public void get(String url) {
        String targetUrl = UrlUtils.getTargetUrl(baseUrl, url);
        LOG.debug("[Get] ({}, base: {}, url: {})", targetUrl, baseUrl, url);

        super.get(targetUrl);
    }

    /**
     * Capability????
     * 
     * @return Capability
     */
    @Override
    public PtlCapabilities getCapabilities() {
        return capabilities;
    }

    @Override
    public <X> X getScreenshotAs(OutputType<X> outputType) throws WebDriverException {
        X screenshot = super.getScreenshotAs(outputType);
        if (environmentConfig != null && environmentConfig.isDebug()) {
            exportDebugScreenshot(screenshot);
        }
        return screenshot;
    }

    private void exportDebugScreenshot(Object screenshot) {
        File imageFile;
        if (screenshot instanceof String) {
            imageFile = OutputType.FILE.convertFromBase64Png((String) screenshot);
        } else if (screenshot instanceof byte[]) {
            imageFile = OutputType.FILE.convertFromPngBytes((byte[]) screenshot);
        } else if (screenshot instanceof File) {
            imageFile = (File) screenshot;
        } else {
            LOG.warn("Unknown OutputType: \"{}\"", screenshot.getClass().getName());
            return;
        }

        // Filename -> logs/screenshots/firefox/43/0000.png
        // FIXME: 2016/01/04 ????
        String filename;
        File dir;
        synchronized (DEBUG_SCREENSHOT_COUNTS) {
            AtomicInteger counter = DEBUG_SCREENSHOT_COUNTS.get(capabilities);
            if (counter == null) {
                counter = new AtomicInteger();
                DEBUG_SCREENSHOT_COUNTS.put(capabilities, counter);
            }
            int count = counter.getAndIncrement();

            filename = String.format(Locale.US, "%04d.png", count);
            dir = new File("logs", "screenshots");
            dir = new File(dir, capabilities.getBrowserName());
            if (!Strings.isNullOrEmpty(capabilities.getVersion())) {
                dir = new File(dir, capabilities.getVersion());
            }

            // First time => delete old files
            if (count == 0 && dir.exists()) {
                try {
                    FileUtils.deleteDirectory(dir);
                } catch (IOException e) {
                    LOG.warn("Cannot delete debug screenshot directory \"" + dir.getAbsolutePath() + "\"", e);
                    return;
                }
            }

            if (!dir.exists() && !dir.mkdirs()) {
                LOG.warn("Debug screenshot persist error. Cannot make directory \"{}\"", dir.getAbsolutePath());
                return;
            }
        }

        try {
            Files.copy(imageFile, new File(dir, filename));
        } catch (IOException e) {
            LOG.warn("Debug screenshot persist error", e);
        }
    }

    /**
     * ???????????
     * 
     * @return ???true????false
     */
    protected boolean canMoveTarget() {
        return true;
    }

    /**
     * body???????????????
     * 
     * @return ???????true????????false
     */
    protected boolean canHideBodyScrollbar() {
        return true;
    }

    /**
     * ????????????????
     * 
     * @return ???????true????????false
     */
    protected boolean canHideElementScrollbar() {
        return true;
    }

    /**
     * ???????????????
     * 
     * @return ?????true??????false
     */
    protected boolean isHideElementsRequired() {
        return true;
    }

    /**
     * ????????????
     * 
     * @return ?????true????????false
     */
    protected boolean canResizeElement() {
        return true;
    }

    /**
     * @see org.openqa.selenium.JavascriptExecutor#executeScript(String, Object...)
     * @param script ?JavaScript
     * @param params ??
     * @param <T> ??
     * @return ?
     */
    @SuppressWarnings("unchecked")
    public <T> T executeJavaScript(String script, Object... params) {
        return (T) executeScript(script, params);
    }

    @Override
    public Object executeScript(String script, Object... args) {
        Object result = super.executeScript(script, args);
        LOG.trace("[Execute script] [{}] (args: {}, result: {})", script, args, result);
        return result;
    }

    //<editor-fold desc="takeScreenshot">

    /**
     * ?????
     * 
     * @param screenshotId ID
     * @return ?
     */
    public ScreenshotResult takeScreenshot(String screenshotId) {
        return takeScreenshot(screenshotId, Collections.singletonList(new CompareTarget()));
    }

    /**
     * ????
     * 
     * @param screenshotId ID
     * @param compareTargets ??
     * @return ?
     */
    public ScreenshotResult takeScreenshot(String screenshotId, CompareTarget[] compareTargets) {
        return takeScreenshot(screenshotId, Arrays.asList(compareTargets));
    }

    /**
     * ????
     * 
     * @param screenshotId ID
     * @param compareTargets ??
     * @return ?
     */
    public ScreenshotResult takeScreenshot(String screenshotId, List<CompareTarget> compareTargets) {
        return takeScreenshot(screenshotId, compareTargets, new ArrayList<DomSelector>());
    }

    /**
     * ???????????
     * 
     * @param screenshotId ID
     * @param compareTargets ??
     * @param hiddenElementsSelectors ?????
     * @return ?
     */
    public ScreenshotResult takeScreenshot(String screenshotId, CompareTarget[] compareTargets,
            DomSelector[] hiddenElementsSelectors) {
        return takeScreenshot(screenshotId, Arrays.asList(compareTargets), Arrays.asList(hiddenElementsSelectors));
    }

    /**
     * ???????
     * 
     * @param arg ?
     * @return ?
     */
    public ScreenshotResult takeScreenshot(ScreenshotArgument arg) {
        return takeScreenshot(arg.getScreenshotId(), arg.getTargets(), arg.getHiddenElementSelectors());
    }

    /**
     * ???????????
     * 
     * @param screenshotId ID
     * @param compareTargets ??
     * @param hiddenElementSelectors ?????
     * @return ?
     */
    public ScreenshotResult takeScreenshot(String screenshotId, List<CompareTarget> compareTargets,
            List<DomSelector> hiddenElementSelectors) {
        List<CompareTarget> cTarget;
        // CompareTargets????BODY??
        if (compareTargets == null || compareTargets.isEmpty()) {
            cTarget = new ArrayList<CompareTarget>(1);
            cTarget.add(new CompareTarget());
        } else {
            cTarget = compareTargets;
        }

        LOG.debug("[TakeScreenshot start] (ssid: {})", screenshotId);

        List<PtlWebElement> hiddenElements = findElementsByDomSelectors(hiddenElementSelectors);
        LOG.trace("[TakeScreenshot] compareTargets: {}, hiddenElementSelectors: {}, hiddenElements: {}",
                compareTargets, hiddenElementSelectors, hiddenElements);

        // CompareTarget => ScreenshotParams
        List<Pair<CompareTarget, ScreenshotParams>> moveTargetParams = new ArrayList<Pair<CompareTarget, ScreenshotParams>>();
        List<Pair<CompareTarget, ScreenshotParams>> nonMoveNoScrollTargetParams = new ArrayList<Pair<CompareTarget, ScreenshotParams>>();
        List<Pair<CompareTarget, ScreenshotParams>> nonMoveScrollTargetParams = new ArrayList<Pair<CompareTarget, ScreenshotParams>>();
        for (CompareTarget compareTarget : cTarget) {
            List<ScreenAreaWrapper> targets = ScreenAreaWrapper.fromArea(compareTarget.getCompareArea(), this,
                    null);
            for (int i = 0; i < targets.size(); i++) {
                ScreenAreaWrapper target = targets.get(i);
                List<ScreenAreaWrapper> excludes = new ArrayList<ScreenAreaWrapper>();
                for (ScreenArea exclude : compareTarget.getExcludes()) {
                    excludes.addAll(target.getChildWrapper(exclude));
                }

                Pair<CompareTarget, ScreenshotParams> pair = Pair.of(compareTarget, new ScreenshotParams(target,
                        excludes, hiddenElements, compareTarget.isMoveTarget(), compareTarget.isScrollTarget(), i));
                if (isMoveTargetRequired(pair.getRight())) {
                    LOG.trace("[TakeScreenshot] MoveTarget: {} (index: {})", target.getParent(), i);
                    moveTargetParams.add(pair);
                } else if (isScrollTargetRequired(pair.getRight())) {
                    LOG.trace("[TakeScreenshot] Non-move scroll target: {} (index: {})", target.getParent(), i);
                    nonMoveScrollTargetParams.add(pair);
                } else {
                    LOG.trace("[TakeScreenshot] Non-move non-scroll target: {} (index: {})", target.getParent(), i);
                    nonMoveNoScrollTargetParams.add(pair);
                }
            }
        }

        // ??
        ScreenshotParams[] additionalParams = extractScreenshotParams(nonMoveNoScrollTargetParams);

        // ????
        ScreenshotParams entireScreenshotParams = new ScreenshotParams(
                ScreenAreaWrapper.fromArea(ScreenArea.of(SelectorType.TAG_NAME, "body"), this, null).get(0),
                new ArrayList<ScreenAreaWrapper>(), hiddenElements, false, false, 0);

        LOG.debug("[TakeScreenshot (entire screenshot start)]");
        TargetResult entireScreenshotResult = getTargetResult(new CompareTarget(), hiddenElementSelectors,
                entireScreenshotParams, additionalParams);
        LOG.debug("[TakeScreenshot (entire screenshot finished)]");
        ScreenshotImage entireScreenshotImage = entireScreenshotResult.getImage();

        List<TargetResult> screenshotResults = new ArrayList<TargetResult>();
        // move???????
        if (nonMoveNoScrollTargetParams.isEmpty()) {
            LOG.debug("[TakeScreenshot (NO non-move non-scroll screenshots)]");
        } else {
            for (Pair<CompareTarget, ScreenshotParams> pair : nonMoveNoScrollTargetParams) {
                LOG.debug("[TakeScreenshot (non-move non-scroll screenshots start)]");
                screenshotResults.add(getTargetResult(pair.getLeft(), hiddenElementSelectors, pair.getRight(),
                        entireScreenshotImage));
                LOG.debug("[TakeScreenshot (non-move non-scroll screenshots finished)]");
            }
        }

        // move??????
        if (nonMoveScrollTargetParams.isEmpty()) {
            LOG.debug("[TakeScreenshot (NO non-move scroll screenshots)]");
        } else {
            LOG.debug("[TakeScreenshot (non-move scroll screenshots start)]");
            List<TargetResult> nonMoveScreenshots = takeNonMoveScreenshots(entireScreenshotResult,
                    hiddenElementSelectors, entireScreenshotParams, nonMoveScrollTargetParams);
            screenshotResults.addAll(nonMoveScreenshots);
            LOG.debug("[TakeScreenshot (non-move scroll screenshots finished)]");
        }

        // move???
        if (moveTargetParams.isEmpty()) {
            LOG.debug("[TakeScreenshot (NO move screenshots)]");
        } else {
            LOG.debug("[TakeScreenshot (move screenshots start)]");
            for (Pair<CompareTarget, ScreenshotParams> pair : moveTargetParams) {
                TargetResult moveScreenshot = takeMoveScreenshots(pair.getLeft(), hiddenElementSelectors,
                        pair.getRight());
                screenshotResults.add(moveScreenshot);
            }
            LOG.debug("[TakeScreenshot (move screenshots finished)]");
        }

        return new ScreenshotResult(screenshotId, screenshotResults, entireScreenshotImage);
    }

    /**
     * isMove?false??????
     * 
     * @param entireScreenshotResult ????TargetResult
     * @param hiddenElementSelectors ??????
     * @param targetParams ?
     * @param entireScreenshotParams ?
     * @return ??TargetResult
     */
    protected List<TargetResult> takeNonMoveScreenshots(TargetResult entireScreenshotResult,
            List<DomSelector> hiddenElementSelectors, ScreenshotParams entireScreenshotParams,
            List<Pair<CompareTarget, ScreenshotParams>> targetParams) {

        LOG.trace(
                "[TakeNonMoveScrollScreenshots] entireScreenshotResult: {}, hiddenElementSelectors: {}, targetParams: {}, entireScreenshotParams: {}",
                entireScreenshotResult, hiddenElementSelectors, targetParams, entireScreenshotParams);

        String[][] overflowStatus = new String[targetParams.size()][2];
        String[] resizeStatus = new String[targetParams.size()];
        int[] partialScrollNums = new int[targetParams.size()];
        int maxPartialScrollNum = 0;

        for (int i = 0; i < targetParams.size(); i++) {
            Pair<CompareTarget, ScreenshotParams> pair = targetParams.get(i);
            PtlWebElement targetElement = pair.getRight().getTarget().getElement();

            // ?hidden??
            // ????????
            if (canHideElementScrollbar()) {
                overflowStatus[i] = targetElement.getOverflowStatus();
                targetElement.hideScrollBar();
            }
            // ????????
            // ??????
            if (canResizeElement()) {
                resizeStatus[i] = targetElement.getResizeStatus();
                targetElement.setNoResizable();
            }

            // ??
            partialScrollNums[i] = targetElement.getScrollNum();
            if (maxPartialScrollNum < partialScrollNums[i]) {
                maxPartialScrollNum = partialScrollNums[i];
            }
            LOG.trace("[TakeNonMoveScrollScreenshots] Partial scroll {} times. ({})", partialScrollNums[i],
                    pair.getLeft().getCompareArea());

            // ?????
            if (partialScrollNums[i] > 0) {
                try {
                    targetElement.scrollTo(0, 0);
                } catch (InterruptedException e) {
                    throw new TestRuntimeException(e);
                }
            }
        }
        LOG.trace("[TakeNonMoveScrollScreenshots] overflow: {}, resize: {}", overflowStatus, resizeStatus);

        // ????
        List<BufferedImage> screenshots = takeNonMoveScrollTargetScreenshots(hiddenElementSelectors,
                entireScreenshotParams, targetParams, maxPartialScrollNum, partialScrollNums);

        List<TargetResult> nonMoveNoScrollTargetResults = new ArrayList<TargetResult>();
        for (int i = 0; i < targetParams.size(); i++) {
            Pair<CompareTarget, ScreenshotParams> pair = targetParams.get(i);

            // ??????
            RectangleArea targetPosition = pair.getRight().getTarget().getArea();
            pair.getRight().getTarget().setArea(new RectangleArea(targetPosition.getX(), targetPosition.getY(),
                    targetPosition.getWidth(), screenshots.get(i).getHeight()));
            LOG.trace("[TakeNonMoveScrollScreenshots] update target position {}",
                    pair.getRight().getTarget().getArea());

            // ??
            ScreenAreaResult targetAreaResult = createScreenAreaResult(pair.getRight().getTarget(),
                    pair.getRight().getIndex());
            List<ScreenAreaResult> excludes = Lists.transform(pair.getRight().getExcludes(),
                    new Function<ScreenAreaWrapper, ScreenAreaResult>() {
                        @Override
                        public ScreenAreaResult apply(ScreenAreaWrapper input) {
                            return createScreenAreaResult(input, null);
                        }
                    });
            TargetResult tResult = new TargetResult(null, targetAreaResult, excludes,
                    isMoveTargetRequired(pair.getRight()), hiddenElementSelectors,
                    new ScreenshotImage(screenshots.get(i)), pair.getLeft().getOptions());
            nonMoveNoScrollTargetResults.add(tResult);
        }

        // ???
        if (canHideElementScrollbar()) {
            for (int i = 0; i < targetParams.size(); i++) {
                PtlWebElement el = targetParams.get(i).getRight().getTarget().getElement();
                el.setOverflowStatus(overflowStatus[i][0], overflowStatus[i][1]);
            }
        }

        // ??
        if (canResizeElement()) {
            for (int i = 0; i < targetParams.size(); i++) {
                PtlWebElement el = targetParams.get(i).getRight().getTarget().getElement();
                if (resizeStatus[i] != null) {
                    el.setResizeStatus(resizeStatus[i]);
                }
            }
        }

        return nonMoveNoScrollTargetResults;
    }

    /**
     * isMove?true?????
     * 
     * @param target ?
     * @param hiddenElementSelectors ??????
     * @param params ?
     * @return ?TargetResult
     */
    protected TargetResult takeMoveScreenshots(CompareTarget target, List<DomSelector> hiddenElementSelectors,
            ScreenshotParams params) {

        LOG.trace("[TakeMoveScreenshot] target: {}, hiddenElementSelectors: {}, params: {}", target,
                hiddenElementSelectors, params);

        // ????
        if (!target.isScrollTarget()) {
            LOG.trace("[TakeMoveScreenshot] partial scroll is not required for ({})", target.getCompareArea());
            return getTargetResult(target, hiddenElementSelectors, params);
        }

        // ???

        PtlWebElement el = params.getTarget().getElement();

        // ????????
        String[] overflowStatus = null;
        if (canHideElementScrollbar()) {
            String[] statuses = el.getOverflowStatus();
            overflowStatus = Arrays.copyOf(statuses, statuses.length);
            // ????
            el.hideScrollBar();
        }

        String resizeStatus = null;
        if (canResizeElement()) {
            // ???????
            resizeStatus = el.getResizeStatus();
            // ????
            el.setNoResizable();
        }

        // ????
        BufferedImage screenshot = takeMoveScrollTargetScreenshot(target, hiddenElementSelectors, params);

        // ??????
        RectangleArea targetPosition = params.getTarget().getArea();
        params.getTarget().setArea(new RectangleArea(targetPosition.getX(), targetPosition.getY(),
                targetPosition.getWidth(), screenshot.getHeight()));
        LOG.trace("[TakeMoveScreenshots] update target position {}", params.getTarget().getArea());

        // TargetResult for target area
        ScreenAreaResult targetAreaResult = createScreenAreaResult(params.getTarget(), params.getIndex());
        // TargetResult for exclude areas
        List<ScreenAreaResult> excludes = Lists.transform(params.getExcludes(),
                new Function<ScreenAreaWrapper, ScreenAreaResult>() {
                    @Override
                    public ScreenAreaResult apply(ScreenAreaWrapper input) {
                        return createScreenAreaResult(input, null);
                    }
                });

        // ????
        if (canHideElementScrollbar()) {
            el.setOverflowStatus(overflowStatus[0], overflowStatus[1]);
        }

        // ???
        if (canResizeElement() && resizeStatus != null) {
            el.setResizeStatus(resizeStatus);
        }

        return new TargetResult(null, targetAreaResult, excludes, params.isMoveTarget(), hiddenElementSelectors,
                new ScreenshotImage(screenshot), target.getOptions());
    }

    /**
     * ?????????????
     * 
     * @param hiddenElementSelectors ??????
     * @param entireScreenshotParams ????
     * @param targetParams ?
     * @param maxPartialScrollNum ??
     * @param partialScrollNums ???
     * @return ??
     */
    private List<BufferedImage> takeNonMoveScrollTargetScreenshots(List<DomSelector> hiddenElementSelectors,
            ScreenshotParams entireScreenshotParams, List<Pair<CompareTarget, ScreenshotParams>> targetParams,
            int maxPartialScrollNum, int[] partialScrollNums) {
        LOG.debug("[TakeNonMoveScrollScreenshots (capture start)] maximum partial scroll times: {}",
                maxPartialScrollNum);

        List<List<BufferedImage>> allTargetScreenshots = new ArrayList<List<BufferedImage>>();
        int targetSize = targetParams.size();
        for (int i = 0; i < targetSize; i++) {
            allTargetScreenshots.add(new ArrayList<BufferedImage>());
        }
        long[] partialScrollAmounts = new long[targetSize];
        long[] partialScrollTops = new long[targetSize];

        // ??
        ScreenshotParams[] additionalParams = extractScreenshotParams(targetParams);

        // ??target?????????target ?
        for (int i = 0; i <= maxPartialScrollNum; i++) {
            // 
            TargetResult entireResult = getTargetResult(new CompareTarget(), hiddenElementSelectors,
                    entireScreenshotParams, additionalParams);
            ScreenshotImage entireScreenshotImage = entireResult.getImage();
            LOG.trace("[TakeNonMoveScrollScreenshots (captured)] ({}) TargetResult: {}", entireScreenshotImage,
                    entireResult);

            // scale???
            if (i == 0) {
                scale = calcScale(getCurrentPageWidth(), entireScreenshotImage.get().getWidth());
                LOG.trace("[TakeNonMoveScrollScreenshots] scale: {}", scale);
            }

            // ?target??
            for (int j = 0; j < targetSize; j++) {
                Pair<CompareTarget, ScreenshotParams> pair = targetParams.get(j);
                PtlWebElement targetElement = pair.getRight().getTarget().getElement();

                // ?????????????
                if (i <= partialScrollNums[j]) {
                    // ??????????
                    TargetResult targetPartResult = getTargetResult(pair.getLeft(), hiddenElementSelectors,
                            pair.getRight(), entireScreenshotImage);
                    allTargetScreenshots.get(j).add(targetPartResult.getImage().get());
                }

                // ??????????????
                if (i < partialScrollNums[j]) {
                    try {
                        long currentScrollAmount = targetElement.scrollNext();
                        if (currentScrollAmount == 0) {
                            partialScrollNums[j] = i;
                        } else {
                            partialScrollAmounts[j] = currentScrollAmount;
                        }
                        LOG.debug("[TakeNonMoveScrollScreenshots] Partial scroll amount({}): {}", j,
                                currentScrollAmount);
                    } catch (InterruptedException e) {
                        throw new TestRuntimeException(e);
                    }

                } else if (i != 0) {
                    // ????????????
                    partialScrollTops[j] = Math.round(targetElement.getCurrentScrollTop());
                }
            }

        }
        LOG.debug("[TakeNonMoveScrollScreenshots (capture finished)]");

        // ????border?
        trimNonMoveBorder(allTargetScreenshots, targetParams, scale);

        // ????padding?
        trimNonMovePadding(allTargetScreenshots, targetParams);

        LOG.debug("[TakeNonMoveScrollScreenshots (image processing start)]");

        // ??????trim
        for (int i = 0; i < allTargetScreenshots.size(); i++) {
            List<BufferedImage> targetScreenshots = allTargetScreenshots.get(i);
            PtlWebElement targetElement = targetParams.get(i).getRight().getTarget().getElement();
            trimBottomImage(targetScreenshots, partialScrollAmounts[i], targetElement, scale);
        }

        List<BufferedImage> screenshots = new ArrayList<>();
        for (int i = 0; i < targetSize; i++) {
            Pair<CompareTarget, ScreenshotParams> pair = targetParams.get(i);
            // Exclude?
            for (ScreenAreaWrapper wrapper : pair.getRight().getExcludes()) {
                wrapper.setArea(wrapper.getArea().move(0, partialScrollTops[i] * scale));
            }
            // ????
            List<BufferedImage> targetScreenshots = allTargetScreenshots.get(i);
            screenshots.add(ImageUtils.verticalMerge(targetScreenshots));
        }

        LOG.debug("[TakeNonMoveScrollScreenshots (image processing finished)]");

        return screenshots;
    }

    /**
     * ?????????????
     * 
     * @param target ?
     * @param hiddenElementSelectors ?????
     * @param params ?
     * @return ??
     */
    private BufferedImage takeMoveScrollTargetScreenshot(CompareTarget target,
            List<DomSelector> hiddenElementSelectors, ScreenshotParams params) {

        PtlWebElement el = params.getTarget().getElement();

        // ???
        long clientHeight = el.getClientHeight();
        LOG.debug("[TakeMoveScreenshot (capture start)] clientHeight: {}", clientHeight);

        double captureTop = 0d;
        long scrollTop = -1L;
        long currentScrollAmount = -1;
        List<Double> allCaptureTop = new ArrayList<Double>();

        List<BufferedImage> images = new ArrayList<BufferedImage>();
        try {
            // ??
            el.scrollTo(0d, 0d);
            // ??
            long currentScrollTop = Math.round(el.getCurrentScrollTop());

            // ???????
            long scrollNum = el.getScrollNum();
            int currentScrollNum = 0;

            while (scrollTop != currentScrollTop && currentScrollNum <= scrollNum) {
                if (currentScrollAmount < 0) {
                    currentScrollAmount = 0;
                } else {
                    currentScrollAmount = currentScrollTop - scrollTop;
                }
                scrollTop = currentScrollTop;
                LOG.trace("[TakeMoveScreenshot] scrollTop: {}, scrollAmount: {}", scrollTop, currentScrollAmount);

                // ??
                BufferedImage image = getTargetResult(target, hiddenElementSelectors, params).getImage().get();
                allCaptureTop.add(captureTop);

                // scale???
                if (currentScrollNum == 0) {
                    scale = calcScale(el.getDoubleValueRect().getWidth(), image.getWidth());
                    LOG.trace("[TakeMoveScreenshot] scale: {}", scale);
                }

                // ??
                images.add(image);

                // ???
                double scrollIncrement = 0;
                scrollIncrement = clientHeight;
                captureTop += scrollIncrement;
                LOG.debug("[TakeMoveScreenshot] Scroll increment: {}", scrollIncrement);

                // ????
                el.scrollNext();
                currentScrollNum++;

                // ??
                currentScrollTop = Math.round(el.getCurrentScrollTop());
            }
        } catch (InterruptedException e) {
            throw new TestRuntimeException(e);
        }
        LOG.debug("[TakeMoveScreenshot (capture finished)]");

        // border?????
        trimMoveBorder(el, images, scale);

        // padding?????
        trimMovePadding(el, images);

        // ??
        trimBottomImage(images, currentScrollAmount, el, scale);

        // Exclude?
        for (ScreenAreaWrapper wrapper : params.getExcludes()) {
            wrapper.setArea(wrapper.getArea().move(0, scrollTop * scale));
        }

        LOG.debug("[TakeMoveScreenshot (image processing start)]");
        BufferedImage screenshot = ImageUtils.verticalMerge(images);
        LOG.debug("[TakeMoveScreenshot (image processing finished)]");

        return screenshot;
    }

    /**
     * ????{@link ScreenshotParams}???????????
     * 
     * @param params ?
     * @return {@link ScreenshotParams}??
     */
    private ScreenshotParams[] extractScreenshotParams(List<Pair<CompareTarget, ScreenshotParams>> params) {
        int size = params.size();
        ScreenshotParams[] extractedParams = new ScreenshotParams[size];
        for (int i = 0; i < size; i++) {
            extractedParams[i] = params.get(i).getRight();
        }
        return extractedParams;
    }

    /**
     * ???????????<br>
     * ????????
     * 
     * @param images ??
     * @param lastScrollAmount ??
     * @param el ??
     * @param currentScale ??
     */
    protected void trimBottomImage(List<BufferedImage> images, long lastScrollAmount, PtlWebElement el,
            double currentScale) {
        LOG.trace("(trimBottomImage) lastScrollAmount: {}, el: {}, currentScroll: {}", lastScrollAmount, el,
                currentScale);

        int size = images.size();
        // ??1???????????
        if (size <= 1) {
            return;
        }

        BufferedImage lastImage = images.get(size - 1);
        int trimTop = calcTrimTop(lastImage.getHeight(), lastScrollAmount, el, currentScale);
        LOG.trace("(trimBottomImage) trim: {}", trimTop);

        if (trimTop > 0 && trimTop < lastImage.getHeight()) {
            images.set(size - 1, ImageUtils.trim(lastImage, trimTop, 0, 0, 0));
        }
    }

    /**
     * ??????????<br>
     * ????????
     * 
     * @param images ??
     * @param lastScrollAmount ??
     * @param el ??
     * @param currentScale ??
     */
    protected void trimRightImage(List<BufferedImage> images, long lastScrollAmount, PtlWebElement el,
            double currentScale) {
        LOG.trace("(trimRightImage) lastScrollAmount: {}, el: {}, currentScroll: {}", lastScrollAmount, el,
                currentScale);

        int size = images.size();
        // ??1???????????
        if (size <= 1) {
            return;
        }

        BufferedImage lastImage = images.get(size - 1);
        int trimLeft = calcTrimLeft(lastImage.getWidth(), lastScrollAmount, el, currentScale);
        LOG.trace("(trimRightImage) trim: {}", trimLeft);

        if (trimLeft > 0 && trimLeft < lastImage.getWidth()) {
            images.set(size - 1, ImageUtils.trim(lastImage, 0, trimLeft, 0, 0));
        }
    }

    /**
     * ????????
     * 
     * @param el ??
     * @param image ?
     * @param num ??
     * @param size ?
     * @return ???BufferedImage
     */
    protected BufferedImage trimTargetBorder(WebElement el, BufferedImage image, int num, int size,
            double currentScale) {
        LOG.trace("(trimTargetBorder) el: {}; image[w: {}, h: {}], num: {}, size: {}", el, image.getWidth(),
                image.getHeight(), num, size);

        WebElementBorderWidth targetBorder = ((PtlWebElement) el).getBorderWidth();

        int trimTop = 0;
        int trimBottom = 0;
        if (size > 1) {
            if (num <= 0) {
                trimBottom = (int) Math.round(targetBorder.getBottom() * currentScale);
            } else if (num >= size - 1) {
                trimTop = (int) Math.round(targetBorder.getTop() * currentScale);
            } else {
                trimBottom = (int) Math.round(targetBorder.getBottom() * currentScale);
                trimTop = (int) Math.round(targetBorder.getTop() * currentScale);
            }
        }

        LOG.trace("(trimTargetBorder) top: {}, bottom: {}", trimTop, trimBottom);
        return ImageUtils.trim(image, trimTop, 0, trimBottom, 0);
    }

    /**
     * ?????Padding???<br>
     * 
     * 
     * @param el ??
     * @param image ?
     * @param num ???
     * @param size ?
     * @return Padding???BufferedImage
     */
    protected BufferedImage trimTargetPadding(WebElement el, BufferedImage image, int num, int size) {
        LOG.trace("(trimTargetPadding) el: {}, image[w: {}, h: {}], num: {}, size: {}", el, image.getWidth(),
                image.getHeight(), num, size);

        WebElementPadding targetPadding = ((PtlWebElement) el).getPadding();

        int trimTop = 0;
        int trimBottom = 0;
        if (size > 1) {
            if (num <= 0) {
                trimBottom = (int) Math.round(targetPadding.getBottom() * scale);
            } else if (num >= size - 1) {
                trimTop = (int) Math.round(targetPadding.getTop() * scale);
            } else {
                trimBottom = (int) Math.round(targetPadding.getBottom() * scale);
                trimTop = (int) Math.round(targetPadding.getTop() * scale);
            }
        }

        LOG.trace("(trimTargetPadding) top: {}, bottom: {}", trimTop, trimBottom);
        return ImageUtils.trim(image, trimTop, 0, trimBottom, 0);
    }

    /**
     * isMove?false???????border?????
     * 
     * @param allTargetScreenshots ??
     * @param targetParams ?
     */
    protected void trimNonMoveBorder(List<List<BufferedImage>> allTargetScreenshots,
            List<Pair<CompareTarget, ScreenshotParams>> targetParams, double currentScale) {
        LOG.trace("[Trim non-move elements' border]");
        for (int i = 0; i < allTargetScreenshots.size(); i++) {
            PtlWebElement targetElement = targetParams.get(i).getRight().getTarget().getElement();
            // ???border?
            if (!targetElement.isBody() && targetParams.get(i).getLeft().isScrollTarget()) {
                List<BufferedImage> targetScreenshots = allTargetScreenshots.get(i);
                for (int j = 0; j < targetScreenshots.size(); j++) {
                    targetScreenshots.set(j, trimTargetBorder(targetElement, targetScreenshots.get(j), j,
                            targetScreenshots.size(), currentScale));
                }
            }

        }
    }

    /**
     * isMove?true???????border?????
     * 
     * @param el ??
     * @param images ??
     */
    protected void trimMoveBorder(WebElement el, List<BufferedImage> images, double currentScale) {
        LOG.trace("[Trim move element's border]");
        for (int i = 0; i < images.size(); i++) {
            images.set(i, trimTargetBorder(el, images.get(i), i, images.size(), currentScale));
        }
    }

    /**
     * isMove?false???????padding?????
     * 
     * @param allTargetScreenshots ??
     * @param targetParams ?
     */
    protected void trimNonMovePadding(List<List<BufferedImage>> allTargetScreenshots,
            List<Pair<CompareTarget, ScreenshotParams>> targetParams) {
        LOG.trace("[Trim non-move elements' padding]");
    }

    /**
     * isMove?true???????padding?????
     * 
     * @param el ??
     * @param images ??
     */
    protected void trimMovePadding(WebElement el, List<BufferedImage> images) {
        LOG.trace("[Trim move element's padding]");
    }

    /**
     * ??????????????????
     * 
     * @param captureTop ???
     * @param captureLeft ???
     * @param windowHeight viewport???
     * @param windowWidth viewport??
     * @param currentScale ??
     * @param img ?
     * @return ?????
     */
    protected BufferedImage trimOverlap(double captureTop, double captureLeft, long windowHeight, long windowWidth,
            double currentScale, BufferedImage img) {
        LOG.trace("(TrimOverlap) image[w: {}, h:{}], top: {}, left: {}, windowWidth: {}, windowHeight: {}",
                img.getWidth(), img.getHeight(), captureTop, captureLeft, windowWidth, windowHeight);

        BufferedImage image = img;

        // ??????????????????
        long calculatedRightValue = Math.round((captureLeft + windowWidth) * currentScale);
        long actualRightValue = Math.round(captureLeft * currentScale) + img.getWidth();
        int trimWidth = calculatedRightValue < actualRightValue ? (int) (actualRightValue - calculatedRightValue)
                : 0;

        // ????????????????
        long calculatedBottomValue = Math.round((captureTop + windowHeight) * currentScale);
        long actualBottomValue = Math.round(captureTop * currentScale) + img.getHeight();
        int trimHeight = calculatedBottomValue < actualBottomValue
                ? (int) (actualBottomValue - calculatedBottomValue)
                : 0;

        // ?????????????
        LOG.trace("(TrimOverlap) right(calc: {}, actual: {}), bottom(calc: {}, actual: {})", calculatedRightValue,
                actualRightValue, calculatedBottomValue, actualBottomValue);
        if (trimWidth > 0 || trimHeight > 0) {
            image = image.getSubimage(0, 0, image.getWidth() - trimWidth, image.getHeight() - trimHeight);
        }

        return image;
    }

    /**
     * ??????{@link TargetResult}???????
     * 
     * @param compareTarget ??
     * @param hiddenElementSelectors ???????
     * @param params ????
     * @param additionalParams ??{@code params}
     *            ?????????????????????
     * @return ?
     */
    protected TargetResult getTargetResult(CompareTarget compareTarget, List<DomSelector> hiddenElementSelectors,
            ScreenshotParams params, ScreenshotParams... additionalParams) {
        ScreenshotImage image = getScreenshotImage(params, additionalParams);

        // TargetResult for target area
        ScreenAreaResult targetAreaResult = createScreenAreaResult(params.getTarget(), params.getIndex());

        // TargetResult for exclude areas
        List<ScreenAreaResult> excludes = Lists.transform(params.getExcludes(),
                new Function<ScreenAreaWrapper, ScreenAreaResult>() {
                    @Override
                    public ScreenAreaResult apply(ScreenAreaWrapper input) {
                        return createScreenAreaResult(input, null);
                    }
                });

        return new TargetResult(null, targetAreaResult, excludes, isMoveTargetRequired(params),
                hiddenElementSelectors, image, compareTarget.getOptions());
    }

    /**
     * ???????{@link TargetResult}??????
     * 
     * @param compareTarget ?
     * @param hiddenElementSelectors ???????
     * @param params ????
     * @param image ?
     * @return {@link TargetResult}
     */
    protected TargetResult getTargetResult(CompareTarget compareTarget, List<DomSelector> hiddenElementSelectors,
            ScreenshotParams params, ScreenshotImage image) {
        BufferedImage bi = image.get();
        ScreenshotImage targetImage;
        RectangleArea targetArea = params.getTarget().getArea();
        if (targetArea.getX() != 0d || targetArea.getY() != 0d || targetArea.getWidth() != bi.getWidth()
                || targetArea.getHeight() != bi.getHeight()) {
            if (targetArea.getWidth() == 0d || targetArea.getHeight() == 0d) {
                targetImage = new ScreenshotImage();
            } else {
                // Crop image and reset elements' area
                targetImage = new ScreenshotImage(cropScreenshotImage(bi, params));
            }
        } else {
            targetImage = image;
        }

        // TargetResult for target area
        ScreenAreaResult targetAreaResult = createScreenAreaResult(params.getTarget(), params.getIndex());

        // TargetResult for exclude areas
        List<ScreenAreaResult> excludes = Lists.transform(params.getExcludes(),
                new Function<ScreenAreaWrapper, ScreenAreaResult>() {
                    @Override
                    public ScreenAreaResult apply(ScreenAreaWrapper input) {
                        return createScreenAreaResult(input, null);
                    }
                });

        return new TargetResult(null, targetAreaResult, excludes, isMoveTargetRequired(params),
                hiddenElementSelectors, targetImage, compareTarget.getOptions());
    }

    /**
     * {@link ScreenAreaWrapper}?{@link ScreenAreaResult}????
     * 
     * @param target ??ScreenAreaWrapper
     * @param index ????
     * @return {@link ScreenAreaResult}
     */
    protected ScreenAreaResult createScreenAreaResult(ScreenAreaWrapper target, Integer index) {
        DomSelector selector = target.getSelector();
        // Rectangle
        if (selector == null) {
            return new ScreenAreaResult(null, target.getArea(), target.getParent());
        }

        // DOM
        return new ScreenAreaResult(new IndexDomSelector(selector, index), target.getArea(), target.getParent());
    }

    /**
     * ??{@link ScreenshotImage}???????
     * 
     * @param params 
     * @param additionalParams ??{@code params}
     *            ?????????????????????
     * @return ??
     */
    protected ScreenshotImage getScreenshotImage(ScreenshotParams params, ScreenshotParams... additionalParams) {
        LOG.trace("[GetScreenshotImage start]");

        Object documentOverflow = null;

        // Hide scrollbar
        if (canHideBodyScrollbar()) {
            // Backup default overflow value
            Map<String, Object> object = executeJavaScript(SCRIPT_GET_DEFAULT_DOCUMENT_OVERFLOW);
            documentOverflow = object.get("overflow");
            LOG.trace("[GetScreenshotImage] Hide scrollbar. origin: {}", documentOverflow);

            executeScript(SCRIPT_SET_DOCUMENT_OVERFLOW, "hidden");
        }

        updateScreenWrapperStatus(0d, 0d, params, additionalParams);
        params.updateInitialArea();

        // Check target element size
        RectangleArea area = params.getTarget().getArea().floor();
        LOG.trace("[GetScreenshotImage] target area size: {}", area);
        if (area.getWidth() == 0d || area.getHeight() == 0d) {
            LOG.debug("[GetScreenshotImage] Target element is empty. (target: {}, index: {})",
                    params.getTarget().getParent(), params.getIndex());
            if (canHideBodyScrollbar()) {
                executeScript(SCRIPT_SET_DOCUMENT_OVERFLOW, documentOverflow);
            }

            return new ScreenshotImage();
        }

        if (isHideElementsRequired()) {
            for (PtlWebElement element : params.getHiddenElements()) {
                element.hide();
            }
        }

        // Do not move if the target is "body" element.
        LOG.debug("[GetScreenshotImage (capture start)]");
        BufferedImage fullScreenshot;
        if (isMoveTargetRequired(params)) {
            fullScreenshot = getScreenshotInternal(params);
        } else {
            fullScreenshot = getScreenshotInternalWithoutMoving(params, additionalParams);
        }
        LOG.debug("[GetScreenshotImage (capture finished)] w: {}, h: {}", fullScreenshot.getWidth(),
                fullScreenshot.getHeight());

        if (isHideElementsRequired()) {
            for (PtlWebElement element : params.getHiddenElements()) {
                element.show();
            }
        }

        // Restore scrollbar
        if (canHideBodyScrollbar()) {
            executeScript(SCRIPT_SET_DOCUMENT_OVERFLOW, documentOverflow);
        }

        // Crop screenshot
        BufferedImage targetImage = cropScreenshotImage(fullScreenshot, params);

        LOG.trace("[GetScreenshotImage finished]");

        // MEMO Driver??????????????????
        return new ScreenshotImage(targetImage);
    }

    /**
     * ????????????????
     * 
     * @param image ?
     * @param params 
     * @return ???
     */
    private BufferedImage cropScreenshotImage(BufferedImage image, ScreenshotParams params) {
        RectangleArea targetArea = params.getTarget().getArea();
        RectangleArea floorTargetArea = targetArea.round();
        LOG.debug("[CropScreenshot] image[w: {}, h: {}], target: {} ({})", image.getWidth(), image.getHeight(),
                floorTargetArea, targetArea);

        // Don't crop image when the target element is "body"
        if (params.getTarget().isBody()) {
            int width = image.getWidth();
            if (width < floorTargetArea.getX() + floorTargetArea.getWidth()) {
                width -= (int) floorTargetArea.getX();
            } else {
                width = (int) floorTargetArea.getWidth();
            }
            int height = image.getHeight();
            if (height < floorTargetArea.getY() + floorTargetArea.getHeight()) {
                height -= (int) floorTargetArea.getY();
            } else {
                height = (int) floorTargetArea.getHeight();
            }
            params.getTarget()
                    .setArea(new RectangleArea(floorTargetArea.getX(), floorTargetArea.getY(), width, height));
            LOG.trace("[CropScreenshot] Did not crop image (element is body). Image area: {}",
                    params.getTarget().getArea());
            return image;
        }

        // (width + x) ? (height + y) ????????
        int maxCropWidth = (int) Math.min(floorTargetArea.getX() + floorTargetArea.getWidth(), image.getWidth());
        int maxCropHeight = (int) Math.min(floorTargetArea.getY() + floorTargetArea.getHeight(), image.getHeight());
        LOG.trace("[CropScreenshot] ({})",
                new RectangleArea((int) floorTargetArea.getX(), (int) floorTargetArea.getY(),
                        maxCropWidth - (int) floorTargetArea.getX(), maxCropHeight - (int) floorTargetArea.getY()));

        BufferedImage targetImage = image.getSubimage((int) floorTargetArea.getX(), (int) floorTargetArea.getY(),
                maxCropWidth - (int) floorTargetArea.getX(), maxCropHeight - (int) floorTargetArea.getY());

        double deltaX = -floorTargetArea.getX();
        double deltaY = -floorTargetArea.getY();

        params.getTarget().setArea(new RectangleArea(0d, 0d, targetImage.getWidth(), targetImage.getHeight()));
        LOG.trace("[CropScreenshot] new area: {}", params.getTarget().getArea());

        LOG.trace("[CropScreenshot] Move excludes. (deltaX: {}, deltaY: {})", deltaX, deltaY);
        for (ScreenAreaWrapper wrapper : params.getExcludes()) {
            wrapper.setArea(wrapper.getArea().move(deltaX, deltaY));
        }

        return targetImage;
    }

    /**
     * ????????????
     * 
     * @param params 
     * @return ???????true
     */
    protected boolean isMoveTargetRequired(ScreenshotParams params) {
        return params.isMoveTarget() && !params.getTarget().isBody() && canMoveTarget();
    }

    /**
     * ??????????
     * 
     * @param params 
     * @return ???????true
     */
    protected boolean isScrollTargetRequired(ScreenshotParams params) {
        return params.isScrollTarget() && !params.getTarget().isBody();
    }

    /**
     * ??????
     * 
     * @param params 
     * @param additionalParams 
     * @return ??
     */
    protected BufferedImage getScreenshotInternalWithoutMoving(ScreenshotParams params,
            ScreenshotParams... additionalParams) {
        BufferedImage image = getMinimumScreenshot(params);
        updateScreenWrapperStatus(0d, 0d, params, additionalParams);

        return image;
    }

    /**
     * ?0, 0)??????
     * 
     * @param params 
     * @param additionalParams 
     * @return ??
     */
    protected BufferedImage getScreenshotInternal(ScreenshotParams params, ScreenshotParams... additionalParams) {
        LOG.debug("[GetMoveScreenshot] target: {}; index: {}", params.getTarget().getParent(), params.getIndex());

        // Backup default body style values
        Map<String, Object> originalStyle = executeJavaScript(SCRIPT_GET_DEFAULT_BODY_STYLE);
        LOG.trace("[GetMoveScreenshot] Original style: {}", originalStyle);

        // Set body width
        executeScript("document.body.style.width = arguments[0]", originalStyle.get("scrollWidth") + "px");

        executeScript(SCRIPT_MOVE_BODY, "absolute", "", "");

        // Get target element position
        ScreenAreaWrapper target = params.getTarget();
        target.updatePosition(getScreenshotScale());
        RectangleArea moveAmount = target.getArea();

        // Move body position
        LOG.debug("[GetMoveScreenshot] Move body (amount = x: {}, y: {})", -moveAmount.getX(), -moveAmount.getY());
        executeScript(SCRIPT_MOVE_BODY, "absolute", String.format("%spx", -moveAmount.getY()),
                String.format("%spx", -moveAmount.getX()));

        BufferedImage image = getMinimumScreenshot(params);

        updateScreenWrapperStatus(moveAmount.getX(), moveAmount.getY(), params, additionalParams);

        // Restore body width
        String width = originalStyle.get("width") == null ? "" : originalStyle.get("width").toString();
        executeScript("document.body.style.width = arguments[0]", width);

        // Restore body position
        String pos = originalStyle.get("position") == null ? "" : originalStyle.get("position").toString();
        String top = originalStyle.get("top") == null ? "" : originalStyle.get("top").toString();
        String left = originalStyle.get("left") == null ? "" : originalStyle.get("left").toString();

        LOG.debug("[GetMoveScreenshot] Restore body position. (width: {}, position: {}, top: {}, left: {})", width,
                pos, top, left);
        executeScript(SCRIPT_MOVE_BODY, pos, top, left);

        return image;
    }

    /**
     * {@link DomSelector}?????????
     * 
     * @param selectors ?????
     * @return ?????
     */
    protected List<PtlWebElement> findElementsByDomSelectors(List<DomSelector> selectors) {
        List<PtlWebElement> elements = new ArrayList<PtlWebElement>();
        if (selectors == null || selectors.isEmpty()) {
            return elements;
        }

        for (DomSelector selector : selectors) {
            for (WebElement element : selector.getType().findElements(this, selector.getValue())) {
                elements.add((PtlWebElement) element);
            }
        }
        return elements;
    }

    /**
     * {@code params}?{@code additionalParams}?????????
     * 
     * @param moveX x???
     * @param moveY y???
     * @param params ?
     * @param additionalParams ?
     */
    private void updateScreenWrapperStatus(double moveX, double moveY, ScreenshotParams params,
            ScreenshotParams... additionalParams) {
        // Scroll to top
        try {
            scrollTo(0d, 0d);
        } catch (InterruptedException e) {
            throw new TestRuntimeException(e);
        }

        double currentScale = getScreenshotScale();
        LOG.debug("(UpdateScreenWrapperStatus) moveX: {}, moveY: {}, scale: {}", moveX, moveY, currentScale);

        updateScreenWrapperStatus(moveX, moveY, currentScale, params);
        for (ScreenshotParams p : additionalParams) {
            updateScreenWrapperStatus(moveX, moveY, currentScale, p);
        }
    }

    /**
     * {@code params}?{@code additionalParams}?????????
     * 
     * @param moveX x???
     * @param moveY y???
     * @param currentScale 
     * @param params ?
     */
    private void updateScreenWrapperStatus(double moveX, double moveY, double currentScale,
            ScreenshotParams params) {
        LOG.trace("(UpdateScreenWrapperStatus) moveX: {}, moveY: {}, scale: {} => {}", moveX, moveY, currentScale,
                params.getTarget().getParent());
        params.getTarget().updatePosition(currentScale, moveX, moveY);

        for (ScreenAreaWrapper wrapper : params.getExcludes()) {
            wrapper.updatePosition(currentScale, moveX, moveY);
        }
    }

    /**
     * ?viewport?????
     * 
     * @return PC???1.0
     */
    protected double getScreenshotScale() {
        return DEFAULT_SCREENSHOT_SCALE;
    }

    /**
     * ????{@link BufferedImage}??????
     * 
     * @return ??
     */
    public BufferedImage getEntirePageScreenshot() {
        return getScreenshotAsBufferedImage();
    }

    /**
     * ?????????{@link BufferedImage}??????<br>
     * 
     * @param params 
     * @return ??
     */
    protected BufferedImage getMinimumScreenshot(ScreenshotParams params) {
        return getEntirePageScreenshot();
    }

    /**
     * ??{@link BufferedImage}???????
     * 
     * @return ??
     */
    protected final BufferedImage getScreenshotAsBufferedImage() {
        try {
            byte[] data = getScreenshotAs(OutputType.BYTES);
            return ImageIO.read(new ByteArrayInputStream(data));
        } catch (IOException e) {
            throw new TestRuntimeException("Screenshot capture error", e);
        }
    }

    //</editor-fold>

    //<editor-fold desc="getWidth/Height">

    /**
     * ???????
     * 
     * @return ?ypx
     */
    public double getCurrentScrollTop() {
        double max = 0d;
        for (String value : SCRIPTS_SCROLL_TOP) {
            try {
                double current = Double.parseDouble(executeScript("return " + value).toString());
                max = Math.max(max, current);
            } catch (Exception e) {
                LOG.debug("(GetCurrentScrollTop) unexpected error", e);
            }
        }
        LOG.trace("(GetCurrentScrollTop) [{}]", max);
        return max;
    }

    /**
     * ???????
     * 
     * @return ?xpx
     */
    public double getCurrentScrollLeft() {
        double max = 0d;
        for (String value : SCRIPTS_SCROLL_LEFT) {
            try {
                double current = Double.parseDouble(executeScript("return " + value).toString());
                max = Math.max(max, current);
            } catch (Exception e) {
                LOG.debug("(GetCurrentScrollLeft) unexpected error", e);
            }
        }
        LOG.trace("(GetCurrentScrollLeft) [{}]", max);
        return max;
    }

    /**
     * ??????
     * 
     * @return ??px
     */
    public long getWindowWidth() {
        return executeJavaScript(GET_WINDOW_WIDTH_SCRIPT);
    }

    /**
     * ???????
     * 
     * @return ???px
     */
    public long getWindowHeight() {
        return executeJavaScript(GET_WINDOW_HEIGHT_SCRIPT);
    }

    /**
     * ?scrollWidth????
     * 
     * @return scrollWidthpx
     */
    public long getScrollWidth() {
        PtlWebElement bodyElement = (PtlWebElement) findElementByTagName("body");
        return executeJavaScript(GET_SCROLL_WIDTH_SCRIPT, bodyElement);
    }

    /**
     * ?scrollHeight????
     * 
     * @return scrollHeightpx
     */
    public long getScrollHeight() {
        PtlWebElement bodyElement = (PtlWebElement) findElementByTagName("body");
        return executeJavaScript(GET_SCROLL_HEIGHT_SCRIPT, bodyElement);
    }

    /**
     * ?????
     * 
     * @return 
     */
    public long getScrollNum() {
        double clientHeight = getWindowHeight();
        double scrollHeight = getScrollHeight() + 1;

        if (clientHeight >= scrollHeight) {
            return 0;
        }

        return (int) (Math.ceil(scrollHeight / clientHeight)) - 1;
    }

    /**
     * ???????
     * 
     * @return ?px
     */
    public long getCurrentPageWidth() {
        // ????
        double scrollTop = getCurrentScrollTop();
        double scrollLeft = getCurrentScrollLeft();
        double pageWidth = 0;

        try {
            // body??
            PtlWebElement bodyElement = (PtlWebElement) findElementByTagName("body");
            scrollTo(0d, 0d);
            Number bodyLeft = executeJavaScript(GET_BODY_LEFT_SCRIPT, bodyElement);

            // ??????body??
            long scrollWidth = getScrollWidth();
            WebElementMargin margin = bodyElement.getMargin();
            // ? + ???+1???
            double totalWidth = scrollWidth + margin.getLeft() + margin.getRight() + 1;
            scrollTo(totalWidth, 0d);
            Number relativeBodyLeft = executeJavaScript(GET_BODY_LEFT_SCRIPT, bodyElement);

            // left??????
            LOG.trace("(GetCurrentPageWidth) relativeBodyLeft: {}, bodyLeft: {}, margin: {}", relativeBodyLeft,
                    bodyLeft, margin.getLeft());
            pageWidth = -relativeBodyLeft.doubleValue() + bodyLeft.doubleValue() + getWindowWidth();

            // ???
            scrollTo(scrollLeft, scrollTop);
        } catch (InterruptedException e) {
            throw new TestRuntimeException(e);
        }

        long result = Math.round(pageWidth);
        LOG.trace("(GetCurrentPageWidth) [{}] ({})", result, pageWidth);
        return result;
    }

    /**
     * ????????
     * 
     * @return ??px
     */
    public long getCurrentPageHeight() {
        // ????
        double scrollTop = getCurrentScrollTop();
        double scrollLeft = getCurrentScrollLeft();
        double pageHeight = 0;

        try {
            // body??
            PtlWebElement bodyElement = (PtlWebElement) findElementByTagName("body");
            scrollTo(0d, 0d);
            Number bodyTop = executeJavaScript(GET_BODY_TOP_SCRIPT, bodyElement);

            // ?????body??
            long scrollHeight = getScrollHeight();
            WebElementMargin margin = bodyElement.getMargin();
            // ?? + ???+1???
            double totalHeight = scrollHeight + margin.getTop() + margin.getBottom() + 1;
            scrollTo(0d, totalHeight);
            Number relativeBodyTop = executeJavaScript(GET_BODY_TOP_SCRIPT, bodyElement);

            // top???????
            LOG.trace("(GetCurrentPageHeight) relativeBodyTop: {}, bodyTop: {}, margin: {}", relativeBodyTop,
                    bodyTop, margin.getTop());
            pageHeight = -relativeBodyTop.doubleValue() + bodyTop.doubleValue() + getWindowHeight();

            // ???
            scrollTo(scrollLeft, scrollTop);
        } catch (InterruptedException e) {
            throw new TestRuntimeException(e);
        }

        long result = Math.round(pageHeight);
        LOG.trace("(GetCurrentPageHeight) [{}] ({})", result, pageHeight);
        return result;
    }

    /**
     * ????????
     * 
     * @param x ?xpx
     * @param y ?ypx
     * @throws InterruptedException ?????
     */
    public void scrollTo(double x, double y) throws InterruptedException {
        executeScript("window.scrollTo(arguments[0], arguments[1])", x, y);
        Thread.sleep(SCROLL_WAIT_MS);
    }

    /**
     * viewport??????
     * 
     * @param windowWidth viewport??
     * @param imageWidth ?
     * @return PC???1
     */
    protected double calcScale(double windowWidth, double imageWidth) {
        return DEFAULT_SCREENSHOT_SCALE;
    }

    /**
     * ??????????????
     * 
     * @param imageHeight ???
     * @param scrollAmount ??
     * @param targetElement 
     * @param currentScale ??
     * @return trim?
     */
    protected int calcTrimTop(int imageHeight, long scrollAmount, PtlWebElement targetElement,
            double currentScale) {
        int borderWidth = 0;
        if (!targetElement.isBody()) {
            WebElementBorderWidth border = targetElement.getBorderWidth();
            borderWidth = (int) Math.round(border.getTop());
        }
        int trimTop = imageHeight - (int) Math.round(scrollAmount * currentScale)
                - (int) Math.round(borderWidth * currentScale);
        LOG.trace("(CalcTrimTop) imageHeight: {}, scrollAmount: {}, element: {} => {}", imageHeight, scrollAmount,
                targetElement, trimTop);
        return trimTop;
    }

    /**
     * ?????????????
     * 
     * @param imageWidth ??
     * @param scrollAmount ??
     * @param targetElement null???
     * @param currentScale ??
     * @return trim?
     */
    protected int calcTrimLeft(int imageWidth, long scrollAmount, PtlWebElement targetElement,
            double currentScale) {
        int borderWidth = 0;
        if (!targetElement.isBody()) {
            WebElementBorderWidth border = targetElement.getBorderWidth();
            borderWidth = (int) Math.round(border.getLeft());
        }
        int trimLeft = imageWidth - (int) Math.round(scrollAmount * currentScale)
                - (int) Math.round(borderWidth * currentScale);
        return trimLeft;
    }

    /**
     * driver??WebElement????
     * 
     * @return WebElement
     */
    protected abstract PtlWebElement newPtlWebElement();

    //</editor-fold>

    //<editor-fold desc="JsonToPtlWebElementConverter">

    /**
     * JSONWebElement???
     */
    static class JsonToPtlWebElementConverter extends JsonToWebElementConverter {

        private final PtlWebDriver driver;

        /**
         * 
         * 
         * @param driver WebDriver
         */
        JsonToPtlWebElementConverter(PtlWebDriver driver) {
            super(driver);
            this.driver = driver;
        }

        @Override
        protected PtlWebElement newRemoteWebElement() {
            PtlWebElement element = driver.newPtlWebElement();
            element.setParent(driver);
            return element;
        }
    }

    //</editor-fold>

}