سجل الاختبارات التلقائية لواجهة مستخدم الفيديو التي تعمل في Chrome بدون رأس

تحية للجميع!

في هذه المقالة ، أود أن أتحدث عن كيفية حل مهمة تسجيل الاختبارات التلقائية للفيديو التي تعمل في Chrome بدون رأس (يجب ألا تكون هناك مشاكل في التسجيل في الوضع غير الرأسي). سيتم النظر في العديد من الأساليب المختلفة ، وسيتم إخبار الصعوبات وطرق حلها.

صياغة المشكلة

  1. تعمل الاختبارات تحت Windows
  2. تستخدم الاختبارات برنامج Selenium Web Driver + Headless Chrome
  3. يتم تشغيل الاختبارات في سلاسل محادثات متعددة

للاختبارات الساقطة ، تحتاج إلى حفظ الفيديو ، بينما

  1. يجب أن ينمو وقت التشغيل بنسبة لا تزيد عن 10٪.
  2. تفعل مع الحد الأدنى من التغييرات في التنفيذ الحالي

إذا كنت مهتمًا بالحل ، فمرحباً بك في القط.

النهج الساذج. لقطات الشاشة


يحتوي إطار الاختبار الخاص بنا على غلاف منخفض المستوى فوق السيلينيوم. لذلك ، كان التنفيذ الأول بسيطًا للغاية وساذجًا للغاية: تمت إضافة الرمز إلى جميع الأماكن التي تغير الصفحة (انقر ، تعيين مربع النص ، التنقل ، إلخ) ، والذي يحفظ لقطة شاشة من خلال برنامج Selenium Web Driver

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();
    ...
}


كان وقت تنفيذ الاختبار طويلاً جدًا. لم أفهم سبب التأخير. على الأرجح ، يرفض السيلينيوم القيام بشيء أثناء حفظ لقطة الشاشة. ربما تساعد حالة أخرى من السيلينيوم ، وتنتهي في نفس الجلسة.

لقطات شاشة في سلسلة منفصلة من خلال Puppeteer


لم يكن من المثير للاهتمام أن أقوم بعمل حالتين من السيلينيوم ، حيث كنت أرغب لفترة طويلة في تجربة العرائس الحادة عمليًا - وهنا وجدت سببًا مناسبًا. على جانب السيلينيوم ، تم إنشاء Puppeteer ، الذي يرتبط ببساطة بالكروم ، الذي تم إنشاؤه بالفعل من خلال السيلينيوم

نص مخفي
var options = new ConnectOptions()
{
    BrowserURL = $"http://127.0.0.1:{debugPort}"
};

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


ذهب الاختبار بطريقته الخاصة من خلال السيلينيوم ، وأخذ 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 . لسوء الحظ ، لم أستطع إقامة علاقات جيدة معها بسرعة. بعد مزيد من عمليات البحث ، تم العثور على حل لـ ScreenCast في mafredri / cdp. تمت إزالة التنقل غير الضروري من المثال الأصلي وأضيفت معلمات الإدخال الضرورية:

نص مخفي
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. بدء تشغيل Chrome عبر Selenium
  2. نبدأ Screencast من خلال الثنائي المجمع - يتصل بـ Chrome ويبدأ في حفظ دفق الإطارات إلى المجلد الذي نحدده
  3. في نهاية الاختبار ، أغلق Chrome - يغلق الثنائي تلقائيًا
  4. إذا فشل الاختبار - قم بإنشاء فيديو
  5. نقوم بتنظيف المجلد مع الإطارات

أعطى هذا النهج أفضل نتيجة من حيث وقت التنفيذ (لا يوجد أي تأخير عمليًا). بالإضافة إلى ذلك ، قدم أقصى قدر من المعلومات حول الاختبار (لا توجد عمليا إطارات مفقودة).

سلبيات

1. دقة منخفضة للشاشة. إذا قمت بإجراء الاختبارات في بعض التدفقات وقمت بتعيين الدقة إلى 2560 * 1440 لمتصفح Chrome ، فسيتم تجاوز المخزن المؤقت المخصص لنقل البيانات.

2. مع زيادة الدقة ، يزداد الحمل على وحدة المعالجة المركزية.

ونتيجة لذلك ، في ظل التسجيل الرقمي للشاشة ، اخترت دقة 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 = نص صغير غير قابل للقراءة عمليًا
كروم 1920 * 1080 -> سكرينكست على 1024 * 576 = نص صغير يُقرأ بصعوبة
كروم 1408 * 792 -> سكرينكست على 1024 * 576 = نص صغير يُقرأ بدون مشاكل

يمكن تحسين الفيديو الناتج 1024 * 576 - إذا تم تصغير الإطارات إلى 1920 * 1080 باستخدام مكتبة PhotoSauce

نص مخفي
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