在无头Chrome中录制视频UI自动测试

大家好!

在本文中,我想谈谈如何解决在无头 Chrome 中录制视频自动测试的任务(在非无头模式下录制应该没有问题)。将考虑几种不同的方法,并说明解决方法的困难和方法。

问题的提法

  1. 测试在Windows下运行
  2. 测试使用Selenium Web驱动程序+无头Chrome
  3. 测试在多个线程中运行

对于失败的测试,您需要保存视频,而

  1. 运行时间的增长不应超过10%。
  2. 在当前实现中进行最少的更改

如果您对解决方案感兴趣,欢迎联系cat。

天真的方法。屏幕截图


我们的测试框架在Selenium上具有低级包装。因此,第一个实现非常简单且非常幼稚:将代码添加到了更改页面的所有位置(单击,设置文本框,导航等),该代码通过Selenium Web驱动程序保存了屏幕截图

Driver.TakeScreenshot().SaveAsFile(screenshotPath);

使用这种方法的测试执行时间已大大增加。原因:保存屏幕快照的操作根本无法快速完成-从0.5秒到几秒钟。

屏幕截图在单独的流中


而不是将代码保存在更改页面的所有位置(单击,设置文本框,导航)的代码,而是添加了一个代码,该代码不断将屏幕快照保存在单独的流中

隐藏文字
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var screenShotDriver = Driver as ITakesScreenshot;
    if (screenShotDriver == null)
    {
        continue;
    }

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


测试执行时间仍然很长。我不明白延迟的原因。Selenium最有可能在保存屏幕截图时拒绝执行任何操作。也许Selenium的另一个实例会有所帮助,在同一会话中结束。

通过Puppeteer在单独的线程中截屏


进行两个Selenium实例并不是一件很有趣的事情,因为我一直想在实践中尝试木偶锐化 -在这里我找到了合适的理由。在Selenium一侧,创建了Puppeteer,它仅连接到已经通过Selenium创建的Chrome

隐藏文字
var options = new ConnectOptions()
{
    BrowserURL = $"http://127.0.0.1:{debugPort}"
};

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


测试通过了Selenium,Puppeteer在单独的线程中截取了屏幕截图

隐藏文字
...
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();
    ...
}


此实现产生了令人鼓舞的结果,执行时间增加了10%。

缺点

  1. 通过Puppeteer保存屏幕截图的时间不是立即的,某些帧会丢失,并且可能会引起解析的兴趣。
  2. 如果Selenium切换了选项卡,则需要通知Puppeteer,否则它将只截屏上面代码中的第一个选项卡(也许有一种查找活动选项卡的方法-您需要查看)。

最初的结果对我来说是个障碍,因此我们继续下一个解决方案。

截屏


Chrome具有有趣的功能-Page.startScreencast根据描述-她只做所需的事情-她投射改变的框架,以便希望的人可以拦截它们并对它们做一些有趣的事情。

在Selenium和Puppeteer中,都可以启动Page.startScreencast,但是不能在一个或另一个中添加处理程序。旅馆业者已经表达了意见-我们正在等待实施。

我尝试使用ChromeDevTools结交朋友不幸的是,我无法迅速与她建立良好的关系。经过进一步的搜索,在mafredri / cdp中找到了ScreenCast的解决方案,从原始示例中删除了不必要的导航,并添加了必要的输入参数:

隐藏文字
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
}


此外,该文件由以下命令编译:

go build -o screencast.exe main.go

我能够在带有测试的C#解决方案中使用它:

隐藏文字
var startInfo = new ProcessStartInfo(screenCastPath)
{
    WindowStyle = ProcessWindowStyle.Minimized,

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

Process.Start(startInfo);


不必要地抛出了用于记录屏幕截图的单独流。工作算法如下:

  1. 通过Selenium启动Chrome
  2. 我们通过组装好的二进制文件开始截屏-它连接到Chrome,然后开始将帧流保存到我们指定的文件夹中
  3. 测试结束时,关闭Chrome-二进制文件自动关闭
  4. 如果测试失败-制作视频
  5. 我们用框架清洁文件夹

就执行时间而言,这种方法给出了最好的结果(几乎没有延迟)。另外,他提供了有关测试的最大信息(实际上没有丢失帧)。

缺点

1.屏幕截图的分辨率较低。如果您在多个流中运行测试并将Chrome的分辨率设置为2560 * 1440,则分配给数据传输缓冲区溢出

2.随着分辨率的提高,CPU上的负载增加。

结果,在截屏下,我选择了1024 * 576的分辨率-在此分辨率下,测试在6个线程中运行良好,处理器在舒适模式下运行(6核i7-5820)。

我们收集视频


它仍然可以收集视频中的帧。为此,我使用了SharpAvi

隐藏文字
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
    }
}


高档图片


由于截屏的分辨率非常小,为1024 * 576,因此您需要为Chrome本身设置一个小的分辨率,否则小文本会出现问题。

Chrome 2560 * 1440-> 1024 * 576的屏幕截图=小文本几乎不可读
Chrome 1920 * 1080-> 1024 * 576的屏幕截图-难以读取小文本
Chrome 1408 * 792-> 1024 * 576的屏幕截图=小文本没有问题

如果使用PhotoSauce库将帧缩小到1920 * 1080,则可以改善最终的视频1024 * 576

隐藏文字
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);
		}
	}
}


结果,获得了以下参数:Chrome在1408 * 792上运行,ScreenCast在1024 * 576上运行,用于观看放大到1920 * 1080的最终视频。单击此处查看最终结果的示例

谢谢


感谢所有阅读过的人-如果对原始问题有更简单的解决方案,请在评论中写下。也可以接受任何批评,包括上述恶意批评。

一切健康迅速结束令人垂涎的限制!

All Articles