web-dev-qa-db-fra.com

Prendre une capture d'écran pleine page en Chrome avec Selenium

Je sais que ce n'était pas possible avant , mais maintenant avec la mise à jour suivante:

https://developers.google.com/web/updates/2017/04/devtools-release-notes#screenshots

cela semble possible en utilisant Chrome Dev Tools.

Est-il possible maintenant d'utiliser Selenium en Java?

15
Majid Laissi

Pour ce faire avec Selenium Webdriver dans Java prend un peu de travail .. Comme suggéré par Florent B. nous devons changer certaines utilisations de classes par le ChromeDriver par défaut pour que cela fonctionne. D'abord, nous devons créer un nouveau DriverCommandExecutor qui ajoute les nouvelles commandes Chrome:

import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.remote.CommandInfo;
import org.openqa.Selenium.remote.http.HttpMethod;
import org.openqa.Selenium.remote.service.DriverCommandExecutor;
import org.openqa.Selenium.remote.service.DriverService;

public class MyChromeDriverCommandExecutor extends DriverCommandExecutor {
    private static final ImmutableMap<String, CommandInfo> CHROME_COMMAND_NAME_TO_URL;

    public MyChromeDriverCommandExecutor(DriverService service) {
        super(service, CHROME_COMMAND_NAME_TO_URL);
    }

    static {
        CHROME_COMMAND_NAME_TO_URL = ImmutableMap.of("launchApp", new CommandInfo("/session/:sessionId/chromium/launch_app", HttpMethod.POST)
        , "sendCommandWithResult", new CommandInfo("/session/:sessionId/chromium/send_command_and_get_result", HttpMethod.POST)
        );
    }
}

Après cela, nous devons créer une nouvelle classe ChromeDriver qui utilisera ensuite cette chose. Nous devons créer la classe parce que l'original n'a pas de constructeur qui nous permet de remplacer l'exécuteur de commande ... Donc la nouvelle classe devient:

import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.Capabilities;
import org.openqa.Selenium.WebDriverException;
import org.openqa.Selenium.chrome.ChromeDriverService;
import org.openqa.Selenium.html5.LocalStorage;
import org.openqa.Selenium.html5.Location;
import org.openqa.Selenium.html5.LocationContext;
import org.openqa.Selenium.html5.SessionStorage;
import org.openqa.Selenium.html5.WebStorage;
import org.openqa.Selenium.interactions.HasTouchScreen;
import org.openqa.Selenium.interactions.TouchScreen;
import org.openqa.Selenium.mobile.NetworkConnection;
import org.openqa.Selenium.remote.FileDetector;
import org.openqa.Selenium.remote.RemoteTouchScreen;
import org.openqa.Selenium.remote.RemoteWebDriver;
import org.openqa.Selenium.remote.html5.RemoteLocationContext;
import org.openqa.Selenium.remote.html5.RemoteWebStorage;
import org.openqa.Selenium.remote.mobile.RemoteNetworkConnection;

public class MyChromeDriver  extends RemoteWebDriver implements LocationContext, WebStorage, HasTouchScreen, NetworkConnection {
    private RemoteLocationContext locationContext;
    private RemoteWebStorage webStorage;
    private TouchScreen touchScreen;
    private RemoteNetworkConnection networkConnection;

    //public MyChromeDriver() {
    //  this(ChromeDriverService.createDefaultService(), new ChromeOptions());
    //}
    //
    //public MyChromeDriver(ChromeDriverService service) {
    //  this(service, new ChromeOptions());
    //}

    public MyChromeDriver(Capabilities capabilities) {
        this(ChromeDriverService.createDefaultService(), capabilities);
    }

    //public MyChromeDriver(ChromeOptions options) {
    //  this(ChromeDriverService.createDefaultService(), options);
    //}

    public MyChromeDriver(ChromeDriverService service, Capabilities capabilities) {
        super(new MyChromeDriverCommandExecutor(service), capabilities);
        this.locationContext = new RemoteLocationContext(this.getExecuteMethod());
        this.webStorage = new RemoteWebStorage(this.getExecuteMethod());
        this.touchScreen = new RemoteTouchScreen(this.getExecuteMethod());
        this.networkConnection = new RemoteNetworkConnection(this.getExecuteMethod());
    }

    @Override
    public void setFileDetector(FileDetector detector) {
        throw new WebDriverException("Setting the file detector only works on remote webdriver instances obtained via RemoteWebDriver");
    }

    @Override
    public LocalStorage getLocalStorage() {
        return this.webStorage.getLocalStorage();
    }

    @Override
    public SessionStorage getSessionStorage() {
        return this.webStorage.getSessionStorage();
    }

    @Override
    public Location location() {
        return this.locationContext.location();
    }

    @Override
    public void setLocation(Location location) {
        this.locationContext.setLocation(location);
    }

    @Override
    public TouchScreen getTouch() {
        return this.touchScreen;
    }

    @Override
    public ConnectionType getNetworkConnection() {
        return this.networkConnection.getNetworkConnection();
    }

    @Override
    public ConnectionType setNetworkConnection(ConnectionType type) {
        return this.networkConnection.setNetworkConnection(type);
    }

    public void launchApp(String id) {
        this.execute("launchApp", ImmutableMap.of("id", id));
    }
}

Il s'agit principalement d'une copie de la classe d'origine, mais avec certains constructeurs désactivés (car une partie du code nécessaire est un package privé). Si vous avez besoin de ces constructeurs, vous devez placer les classes dans le package org.openqa.Selenium.chrome.

Avec ces modifications, vous pouvez appeler le code requis, comme indiqué par Florent B., mais maintenant dans Java avec l'API Selenium:

import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.remote.Command;
import org.openqa.Selenium.remote.Response;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import Java.awt.image.BufferedImage;
import Java.io.ByteArrayInputStream;
import Java.io.File;
import Java.io.FileOutputStream;
import Java.io.IOException;
import Java.util.Base64;
import Java.util.HashMap;
import Java.util.Map;

public class ChromeExtender {
    @Nonnull
    private MyChromeDriver m_wd;

    public ChromeExtender(@Nonnull MyChromeDriver wd) {
        m_wd = wd;
    }

    public void takeScreenshot(@Nonnull File output) throws Exception {
        Object visibleSize = evaluate("({x:0,y:0,width:window.innerWidth,height:window.innerHeight})");
        Long visibleW = jsonValue(visibleSize, "result.value.width", Long.class);
        Long visibleH = jsonValue(visibleSize, "result.value.height", Long.class);

        Object contentSize = send("Page.getLayoutMetrics", new HashMap<>());
        Long cw = jsonValue(contentSize, "contentSize.width", Long.class);
        Long ch = jsonValue(contentSize, "contentSize.height", Long.class);

        /*
         * In chrome 61, delivered one day after I wrote this comment, the method forceViewport was removed.
         * I commented it out here with the if(false), and hopefully wrote a working alternative in the else 8-/
         */
        if(false) {
            send("Emulation.setVisibleSize", ImmutableMap.of("width", cw, "height", ch));
            send("Emulation.forceViewport", ImmutableMap.of("x", Long.valueOf(0), "y", Long.valueOf(0), "scale", Long.valueOf(1)));
        } else {
            send("Emulation.setDeviceMetricsOverride",
                ImmutableMap.of("width", cw, "height", ch, "deviceScaleFactor", Long.valueOf(1), "mobile", Boolean.FALSE, "fitWindow", Boolean.FALSE)
            );
            send("Emulation.setVisibleSize", ImmutableMap.of("width", cw, "height", ch));
        }

        Object value = send("Page.captureScreenshot", ImmutableMap.of("format", "png", "fromSurface", Boolean.TRUE));

        // Since chrome 61 this call has disappeared too; it does not seem to be necessary anymore with the new code.
        // send("Emulation.resetViewport", ImmutableMap.of()); 
        send("Emulation.setVisibleSize", ImmutableMap.of("x", Long.valueOf(0), "y", Long.valueOf(0), "width", visibleW, "height", visibleH));

        String image = jsonValue(value, "data", String.class);
        byte[] bytes = Base64.getDecoder().decode(image);

        try(FileOutputStream fos = new FileOutputStream(output)) {
            fos.write(bytes);
        }
    }

    @Nonnull
    private Object evaluate(@Nonnull String script) throws IOException {
        Map<String, Object> param = new HashMap<>();
        param.put("returnByValue", Boolean.TRUE);
        param.put("expression", script);

        return send("Runtime.evaluate", param);
    }

    @Nonnull
    private Object send(@Nonnull String cmd, @Nonnull Map<String, Object> params) throws IOException {
        Map<String, Object> exe = ImmutableMap.of("cmd", cmd, "params", params);
        Command xc = new Command(m_wd.getSessionId(), "sendCommandWithResult", exe);
        Response response = m_wd.getCommandExecutor().execute(xc);

        Object value = response.getValue();
        if(response.getStatus() == null || response.getStatus().intValue() != 0) {
            //System.out.println("resp: " + response);
            throw new MyChromeDriverException("Command '" + cmd + "' failed: " + value);
        }
        if(null == value)
            throw new MyChromeDriverException("Null response value to command '" + cmd + "'");
        //System.out.println("resp: " + value);
        return value;
    }

    @Nullable
    static private <T> T jsonValue(@Nonnull Object map, @Nonnull String path, @Nonnull Class<T> type) {
        String[] segs = path.split("\\.");
        Object current = map;
        for(String name: segs) {
            Map<String, Object> cm = (Map<String, Object>) current;
            Object o = cm.get(name);
            if(null == o)
                return null;
            current = o;
        }
        return (T) current;
    }
}

Cela vous permet d'utiliser les commandes comme spécifié et crée un fichier avec une image au format png à l'intérieur. Vous pouvez bien sûr également créer directement une BufferedImage en utilisant ImageIO.read () sur les octets.

3
fjalvingh

Oui, il est possible de faire une capture d'écran pleine page avec Selenium depuis Chrome v59. Le pilote Chrome a deux nouveaux points de terminaison pour appeler directement l'API DevTools:

/session/:sessionId/chromium/send_command_and_get_result
/session/:sessionId/chromium/send_command

L'API Selenium n'implémente pas ces commandes, vous devrez donc les envoyer directement avec l'exécuteur sous-jacent. Ce n'est pas simple, mais au moins il est possible de produire exactement le même résultat que DevTools.

Voici un exemple avec python travaillant sur une instance locale ou distante:

from Selenium import webdriver
import json, base64

capabilities = {
  'browserName': 'chrome',
  'chromeOptions':  {
    'useAutomationExtension': False,
    'args': ['--disable-infobars']
  }
}

driver = webdriver.Chrome(desired_capabilities=capabilities)
driver.get("https://stackoverflow.com/questions")

png = chrome_takeFullScreenshot(driver)

with open(r"C:\downloads\screenshot.png", 'wb') as f:
  f.write(png)

, et le code pour prendre une capture d'écran pleine page:

def chrome_takeFullScreenshot(driver) :

  def send(cmd, params):
    resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
    url = driver.command_executor._url + resource
    body = json.dumps({'cmd':cmd, 'params': params})
    response = driver.command_executor._request('POST', url, body)
    return response.get('value')

  def evaluate(script):
    response = send('Runtime.evaluate', {'returnByValue': True, 'expression': script})
    return response['result']['value']

  metrics = evaluate( \
    "({" + \
      "width: Math.max(window.innerWidth, document.body.scrollWidth, document.documentElement.scrollWidth)|0," + \
      "height: Math.max(innerHeight, document.body.scrollHeight, document.documentElement.scrollHeight)|0," + \
      "deviceScaleFactor: window.devicePixelRatio || 1," + \
      "mobile: typeof window.orientation !== 'undefined'" + \
    "})")
  send('Emulation.setDeviceMetricsOverride', metrics)
  screenshot = send('Page.captureScreenshot', {'format': 'png', 'fromSurface': True})
  send('Emulation.clearDeviceMetricsOverride', {})

  return base64.b64decode(screenshot['data'])

Avec Java:

public static void main(String[] args) throws Exception {

    ChromeOptions options = new ChromeOptions();
    options.setExperimentalOption("useAutomationExtension", false);
    options.addArguments("disable-infobars");

    ChromeDriverEx driver = new ChromeDriverEx(options);

    driver.get("https://stackoverflow.com/questions");
    File file = driver.getFullScreenshotAs(OutputType.FILE);
}
import Java.lang.reflect.Method;
import Java.util.Map;
import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.OutputType;
import org.openqa.Selenium.chrome.ChromeDriver;
import org.openqa.Selenium.chrome.ChromeDriverService;
import org.openqa.Selenium.chrome.ChromeOptions;
import org.openqa.Selenium.remote.CommandInfo;
import org.openqa.Selenium.remote.HttpCommandExecutor;
import org.openqa.Selenium.remote.http.HttpMethod;


public class ChromeDriverEx extends ChromeDriver {

    public ChromeDriverEx() throws Exception {
        this(new ChromeOptions());
    }

    public ChromeDriverEx(ChromeOptions options) throws Exception {
        this(ChromeDriverService.createDefaultService(), options);
    }

    public ChromeDriverEx(ChromeDriverService service, ChromeOptions options) throws Exception {
        super(service, options);
        CommandInfo cmd = new CommandInfo("/session/:sessionId/chromium/send_command_and_get_result", HttpMethod.POST);
        Method defineCommand = HttpCommandExecutor.class.getDeclaredMethod("defineCommand", String.class, CommandInfo.class);
        defineCommand.setAccessible(true);
        defineCommand.invoke(super.getCommandExecutor(), "sendCommand", cmd);
    }

    public <X> X getFullScreenshotAs(OutputType<X> outputType) throws Exception {
        Object metrics = sendEvaluate(
            "({" +
            "width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0," +
            "height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0," +
            "deviceScaleFactor: window.devicePixelRatio || 1," +
            "mobile: typeof window.orientation !== 'undefined'" +
            "})");
        sendCommand("Emulation.setDeviceMetricsOverride", metrics);
        Object result = sendCommand("Page.captureScreenshot", ImmutableMap.of("format", "png", "fromSurface", true));
        sendCommand("Emulation.clearDeviceMetricsOverride", ImmutableMap.of());
        String base64EncodedPng = (String)((Map<String, ?>)result).get("data");
        return outputType.convertFromBase64Png(base64EncodedPng);
    }

    protected Object sendCommand(String cmd, Object params) {
        return execute("sendCommand", ImmutableMap.of("cmd", cmd, "params", params)).getValue();
    }

    protected Object sendEvaluate(String script) {
        Object response = sendCommand("Runtime.evaluate", ImmutableMap.of("returnByValue", true, "expression", script));
        Object result = ((Map<String, ?>)response).get("result");
        return ((Map<String, ?>)result).get("value");
    }
}
23
Florent B.