Enregistrer les autotests de l'interface utilisateur vidéo dans Chrome sans tête

Bonjour à tous!

Dans cet article, je veux parler de la façon dont le problème de l'enregistrement des autotests vidéo fonctionnant dans Chrome sans tête a été résolu (il ne devrait pas y avoir de problème avec l'enregistrement en mode sans tête). Plusieurs approches différentes seront envisagées, et les difficultés et les moyens de les résoudre seront expliqués.

Formulation du problème

  1. Tests exécutés sous Windows
  2. Les tests utilisent Selenium Web Driver + Chrome sans tête
  3. Les tests s'exécutent dans plusieurs threads

Pour les tests ratés, vous devez enregistrer la vidéo, tout en

  1. Le temps d'exécution ne devrait pas augmenter de plus de 10%.
  2. Faire avec un minimum de changements dans l'implémentation actuelle

Si vous êtes intéressé par une solution, bienvenue chez cat.

L'approche naïve. Captures d'écran


Notre cadre de test a un wrapper de bas niveau sur le sélénium. Par conséquent, la première implémentation était très simple et extrêmement naïve: du code a été ajouté à tous les endroits qui modifient la page (clic, zone de texte, navigation, etc.), ce qui enregistre une capture d'écran via le pilote Web Selenium

Driver.TakeScreenshot().SaveAsFile(screenshotPath);

Le temps d'exécution des tests avec cette approche a considérablement augmenté. Raison: l'opération d'enregistrement de la capture d'écran ne fonctionne pas du tout rapidement - de 0,5 seconde à plusieurs secondes.

Captures d'écran dans un flux séparé


Au lieu du code qui enregistre les captures d'écran dans tous les endroits qui changent la page (Cliquez, Définir la zone de texte, Naviguer), un code a été ajouté qui enregistre constamment les captures d'écran dans un flux séparé

Texte masqué
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var screenShotDriver = Driver as ITakesScreenshot;
    if (screenShotDriver == null)
    {
        continue;
    }

    var screenShot = screenShotDriver.GetScreenshot();
    ...
}


Le temps d'exécution du test était encore très long. Je n'ai pas compris la raison du retard. Il est fort probable que Selenium refuse de faire quelque chose pendant la capture d'écran. Peut-être qu'une autre instance de Sélénium aiderait, se terminant dans la même session.

Captures d'écran dans un fil séparé via Puppeteer


Ce n'était pas très intéressant de faire deux exemples de Sélénium, car je voulais depuis longtemps essayer les marionnettistes dans la pratique - et ici j'ai trouvé une raison appropriée. Du côté de Selenium, Puppeteer a été créé, qui se connectait simplement à Chrome, déjà créé via Selenium

Texte masqué
var options = new ConnectOptions()
{
    BrowserURL = $"http://127.0.0.1:{debugPort}"
};

_puppeteerBrowser = Puppeteer.ConnectAsync(options).GetAwaiter().GetResult();


Le test a fait son chemin à travers Selenium et Puppeteer a pris des captures d'écran dans un fil séparé

Texte masqué
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var pages = _puppeteerBrowser.PagesAsync().GetAwaiter().GetResult();
    if (pages.Length <= 0)
    {
        continue;
    }
    
    var page = pages[0];
    
    page.SetViewportAsync(new ViewPortOptions
    {
        Width = screenWidth,
        Height = screenHeight
    }).GetAwaiter().GetResult();
    
    var screen = page.ScreenshotStreamAsync().GetAwaiter().GetResult();
    ...
}


Cette mise en œuvre a donné des résultats encourageants, le temps d'exécution a augmenté de 10%.

Moins

  1. Le temps pour enregistrer des captures d'écran via Puppeteer n'est pas instantané, certains des cadres seront perdus et ils peuvent s'avérer intéressants à analyser.
  2. Si Selenium change d'onglet, vous devez en informer Puppeteer, sinon il ne capture que le premier onglet du code ci-dessus (il existe peut-être un moyen de trouver l'onglet actif - vous devez le regarder).

Le premier inconvénient s'est avéré être un blocage pour moi, nous passons donc à la solution suivante.

Screencast


Chrome a une fonctionnalité intéressante - Page.startScreencast . Selon la description - elle fait juste ce qui est nécessaire - elle jette les cadres modifiés afin que ceux qui le souhaitent puissent les intercepter et faire quelque chose d'intéressant avec eux.

Dans Selenium et Puppeteer, vous pouvez démarrer Page.startScreencast, mais vous ne pouvez pas ajouter de gestionnaires dans l'un ou l'autre. Le hoteller est déjà exprimé - nous attendons la mise en œuvre.

J'ai essayé de me faire des amis avec la bibliothèque ChromeDevTools . Malheureusement, je n'ai pas pu rapidement établir de bonnes relations avec elle. Après d'autres recherches, une solution a été trouvée pour ScreenCast dans mafredri / cdp. La navigation inutile a été supprimée de l'exemple d'origine et les paramètres d'entrée nécessaires ont été ajoutés:

Texte masqué
package main

import (
    "os"
    "context"
    "fmt"
    "io/ioutil"
    "log"
    "time"
    "flag"

    "github.com/mafredri/cdp"
    "github.com/mafredri/cdp/devtool"
    "github.com/mafredri/cdp/protocol/page"
    "github.com/mafredri/cdp/rpcc"
)

func main() {

    folderPtr := flag.String("folder", "", "folder path for screenshots: example c:\\temp\\screens\\")
    chromePtr := flag.String("chrome", "http://localhost:9222", "chrome connection - example: http://localhost:9222")
    
    widthPtr := flag.Int("width", 1280, "screencast width")
    heightPtr := flag.Int("height", 720, "screencast height")
    qualityPtr := flag.Int("quality", 100, "screencast quality")
    
    flag.Parse()

    if err := run(*folderPtr, *chromePtr, *widthPtr, *heightPtr, *qualityPtr); err != nil {
        panic(err)
    }
}

func run(folder string, chromeConnection string, width int, height int, quality int) error {
    ctx, cancel := context.WithCancel(context.TODO())
    defer cancel()
    
    chromePath := chromeConnection
    folderPath := folder

    devt := devtool.New(chromePath)

    pageTarget, err := devt.Get(ctx, devtool.Page)
    if err != nil {
        return err
    }

    conn, err := rpcc.DialContext(ctx, pageTarget.WebSocketDebuggerURL)
    if err != nil {
        return err
    }
    defer conn.Close()

    c := cdp.NewClient(conn)

    err = c.Page.Enable(ctx)
    if err != nil {
        return err
    }

    // Start listening to ScreencastFrame events.
    screencastFrame, err := c.Page.ScreencastFrame(ctx)
    if err != nil {
        return err
    }

    go func() {
        defer screencastFrame.Close()

        for {
            ev, err := screencastFrame.Recv()
            if err != nil {
                log.Printf("Failed to receive ScreencastFrame: %v", err)
                os.Exit(0)
            }
            log.Printf("Got frame with sessionID: %d: %+v", ev.SessionID, ev.Metadata)

            err = c.Page.ScreencastFrameAck(ctx, page.NewScreencastFrameAckArgs(ev.SessionID))
            if err != nil {
                log.Printf("Failed to ack ScreencastFrame: %v", err)
                os.Exit(0)
            }

            // Write to screencast_frame-[timestamp].png.
            name := fmt.Sprintf("screencast_frame-%d.png", ev.Metadata.Timestamp.Time().Unix())
            
            filePath := folderPath + name

            // Write the frame to file (without blocking).
            go func() {
                err = ioutil.WriteFile(filePath, ev.Data, 0644)
                if err != nil {
                    log.Printf("Failed to write ScreencastFrame to %q: %v", name, err)
                }
            }()
        }
    }()

    screencastArgs := page.NewStartScreencastArgs().
        SetQuality(quality).
        SetMaxWidth(width).
        SetMaxHeight(height).
        SetEveryNthFrame(1).
        SetFormat("png")
    err = c.Page.StartScreencast(ctx, screencastArgs)
    if err != nil {
        return err
    }

    // Random delay for our screencast.
    time.Sleep(600 * time.Second)

    err = c.Page.StopScreencast(ctx)
    if err != nil {
        return err
    }

    return nil
}


De plus, ce fichier a été compilé par la commande:

go build -o screencast.exe main.go

Et j'ai pu l'utiliser dans une solution C # avec des tests:

Texte masqué
var startInfo = new ProcessStartInfo(screenCastPath)
{
    WindowStyle = ProcessWindowStyle.Minimized,

    Arguments = $"-folder={_framesFolderPath} " +
                $"-chrome=http://localhost:{_debugPort} " +
                "-width=1024 " +
                "-height=576 " +
                "-quality=0"
};

Process.Start(startInfo);


Un flux distinct pour l'enregistrement des captures d'écran a été rejeté comme inutile. L'algorithme de travail était le suivant:

  1. Démarrage de Chrome via Selenium
  2. Nous démarrons Screencast via le binaire assemblé - il se connecte à Chrome et commence à enregistrer le flux d'images dans le dossier que nous spécifions
  3. À la fin du test, fermez Chrome - le binaire se ferme automatiquement
  4. Si le test échoue - créez une vidéo
  5. Nous nettoyons le dossier avec des cadres

Cette approche a donné le meilleur résultat en termes de temps d'exécution (il n'y a pratiquement aucun retard). De plus, il a fourni un maximum d'informations sur le test (il n'y a pratiquement pas de trames perdues).

Inconvénients

1. Basse résolution pour la capture d'écran. Si vous exécutez les tests dans quelques flux et définissez la résolution sur 2560 * 1440 pour Chrome, le tampon alloué pour le transfert de données va déborder .

2. Avec une augmentation de la résolution, la charge sur le CPU augmente.

En conséquence, sous screencast, j'ai choisi la résolution de 1024 * 576 - sur cette résolution, les tests ont bien fonctionné en 6 threads, le processeur a fonctionné en mode confortable (6 core i7-5820).

Nous collectons des vidéos


Il reste à collecter des images dans la vidéo. Pour cela, j'ai utilisé la bibliothèque SharpAvi

Texte masqué
private void GenerateVideoFromScreens(string videoPath)
{
    try
    {
        var videoWriter = new AviWriter(videoPath) { FramesPerSecond = 1, EmitIndex1 = true };

        var videoStream = videoWriter.AddMotionJpegVideoStream(1024, 576);

        var screens = new DirectoryInfo(_framesFolderPath).GetFiles().OrderBy(f => f.CreationTimeUtc.Ticks).ToList();
        foreach (var screen in screens)
        {
            try
            {
                using (var bmp = new Bitmap(screen.FullName))
                {
                    var bits = bmp.LockBits(new Rectangle(0, 0, videoStream.Width, videoStream.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
                    var videoFrame = new byte[videoStream.Width * videoStream.Height * 4];
                    Marshal.Copy(bits.Scan0, videoFrame, 0, videoFrame.Length);
                    bmp.UnlockBits(bits);

                    videoStream.WriteFrameAsync(
                        true,
                        videoFrame,
                        0,
                        videoFrame.Length).GetAwaiter().GetResult();
                }
            }
            catch(Exception ex)
            {
                // ignore all video related errors per frame
            }
        }

        videoWriter.Close();
    }
    catch
    {
        // ignore all video related errors per streams
    }
}


Photos haut de gamme


Étant donné que la résolution de la capture d'écran est très petite 1024 * 576, vous devez définir une petite résolution pour Chrome lui-même, sinon il y aura des problèmes avec le petit texte.

Chrome 2560 * 1440 -> screencast à 1024 * 576 = le petit texte est pratiquement illisible
Chrome 1920 * 1080 -> screencast à 1024 * 576 = le petit texte est lu avec difficulté
Chrome 1408 * 792 -> screencast à 1024 * 576 = le petit texte est lu sans problème

La vidéo résultante 1024 * 576 peut être améliorée - si les images sont réduites à 1920 * 1080 à l'aide de la bibliothèque PhotoSauce

Texte masqué
public Bitmap ResizeImage(Bitmap bitmap, int width)
{
	using (var inStream = new MemoryStream())
	{
		bitmap.Save(inStream, ImageFormat.Png);
		inStream.Position = 0;
		using (MemoryStream outStream = new MemoryStream())
		{
			var settings = new ProcessImageSettings { Width = width };
			MagicImageProcessor.ProcessImage(inStream, outStream, settings);
			return new Bitmap(outStream);
		}
	}
}


En conséquence, les paramètres suivants ont été obtenus: Chrome fonctionne en 1408 * 792, ScreenCast en 1024 * 576, la vidéo finale pour la visualisation haut de gamme à 1920 * 1080. Cliquez ici pour voir un exemple du résultat final.

remercier


Merci à tous ceux qui ont lu - s'il existe une solution plus simple au problème d'origine, veuillez écrire dans le commentaire. Toute critique est également acceptée, y compris les critiques malveillantes décrites ci-dessus.

Toute santé et fin rapide aux restrictions convoitées!

All Articles