Repository: DevExpress/testcafe (js)

Issue:
regression of video behaviour
### What is your Scenario?

this is a continuance of https://github.com/DevExpress/testcafe/issues/7189 we have tried 1.20.1-rc2 and while we are seeing more of the videos that we were in 1.20 we are still loosing the essential ends of videos

### What is the Current behavior?

WHen 40 tests (run times from a few seconds to a couple of minutes) are run concurrently (3 or 4 at once) many loose the endings on the video recordings.

### What is the Expected behavior?

all videos are complete (as in 1.18 and 1.19)

### What is your public website URL? (or attach your complete example)

not relevant

### What is your TestCafe test code?

irrelevant (I can provide zipped comparative videos if required)

### Your complete configuration file

we have an extensive set of config.js files

### Your complete test report

report irrelevant - tests pass correctly

### Screenshots

_No response_

### Steps to Reproduce

1. gather a reasonably sized suite of tests which run for a variety of times, set up video recording, run against headless chrome/chromium
2. run with concurrency of 3 or 4
3. watch videos - ones from the middle onwards - especially longer running ones are cut short


### TestCafe version

1.20.1-rc2 

### Node.js version

16.14.2

### Command-line arguments

irrelevant - run by internal script

### Browser name(s) and version(s)

Version 104.0.5112.81 (Official Build) (64-bit)

### Platform(s) and version(s)

windows 11 (1.20 was also failing on alpine)

### Other

 https://github.com/DevExpress/testcafe/issues/7189

Pull request description:
fix video recording in concurrency mode (closes #7218)
The main idea - call the `client.Page.screencastFrameAck` method for last saved frame only 1 time.

Patch:
diff --git a/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts b/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts
index bd527ca74..ed614e5b1 100644
--- a/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts
+++ b/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts
@@ -386,7 +386,8 @@ export class BrowserClient {
     }
 
     public async getVideoFrameData (): Promise<Buffer | null> {
-        const currentVideoFrame = this._videoFramesBuffer.shift() || this._lastFrame;
+        const firstFrameFromBuffer = this._videoFramesBuffer.shift();
+        const currentVideoFrame    = firstFrameFromBuffer || this._lastFrame;
 
         if (!currentVideoFrame)
             return null;
@@ -399,7 +400,8 @@ export class BrowserClient {
         if (!client)
             return null;
 
-        await client.Page.screencastFrameAck({ sessionId: currentVideoFrame.sessionId });
+        if (firstFrameFromBuffer)
+            await client.Page.screencastFrameAck({ sessionId: currentVideoFrame.sessionId });
 
         return Buffer.from(currentVideoFrame.data, 'base64');
     }
diff --git a/src/errors/runtime/index.js b/src/errors/runtime/index.js
index cee99ba31..5c087b828 100644
--- a/src/errors/runtime/index.js
+++ b/src/errors/runtime/index.js
@@ -5,6 +5,7 @@ import renderTemplate from '../../utils/render-template';
 import renderCallsiteSync from '../../utils/render-callsite-sync';
 import { RUNTIME_ERRORS } from '../types';
 import getRenderers from '../../utils/get-renderes';
+import util from 'util';
 
 const ERROR_SEPARATOR = '\n\n';
 
@@ -128,10 +129,22 @@ export class CompositeError extends Error {
 
 export class ReporterPluginError extends GeneralError {
     constructor ({ name, method, originalError }) {
-        const code = RUNTIME_ERRORS.uncaughtErrorInReporter;
+        const code          = RUNTIME_ERRORS.uncaughtErrorInReporter;
+        const preparedStack = ReporterPluginError._prepareStack(originalError);
 
-        super(code, method, name, originalError.stack);
+        super(code, method, name, preparedStack);
     }
+
+    static _prepareStack (err) {
+        if (!err?.stack) {
+            const inspectedObject = util.inspect(err);
+
+            return `No stack trace is available for a raised error.\nRaised error object inspection:\n${inspectedObject}`;
+        }
+
+        return err.stack;
+    }
+
 }
 
 export class TimeoutError extends GeneralError {
diff --git a/src/reporter/index.ts b/src/reporter/index.ts
index dea3aeec8..79993fc7d 100644
--- a/src/reporter/index.ts
+++ b/src/reporter/index.ts
@@ -32,6 +32,7 @@ import fs from 'fs';
 import MessageBus from '../utils/message-bus';
 import BrowserConnection from '../browser/connection';
 import { Dictionary } from '../configuration/interfaces';
+import debug from 'debug';
 
 interface PendingPromise {
     resolve: Function | null;
@@ -121,6 +122,8 @@ interface ReportWarningEventArguments {
     actionId?: string;
 }
 
+const debugLog = debug('testcafe:reporter');
+
 export default class Reporter {
     public readonly plugin: ReporterPluginHost;
     public readonly messageBus: MessageBus;
@@ -175,6 +178,9 @@ export default class Reporter {
                 originalError,
             });
 
+            debugLog('Plugin error: %O', uncaughtError);
+            debugLog('Plugin error: initialObject: %O', initialObject);
+
             if (initialObject)
                 await initialObject.emit('error', uncaughtError);
             else
diff --git a/src/video-recorder/process.js b/src/video-recorder/process.js
index bcf655017..ecec9a024 100644
--- a/src/video-recorder/process.js
+++ b/src/video-recorder/process.js
@@ -36,7 +36,7 @@ const DEFAULT_OPTIONS = {
 
 const FFMPEG_START_DELAY = 500;
 
-const DELAY_AFTER_EMPTY_FRAME = 50;
+const DELAY_AFTER_EMPTY_FRAME = 20;
 
 export default class VideoRecorder extends AsyncEmitter {
     constructor (basePath, ffmpegPath, connection, customOptions) {


Test patch:
diff --git a/src/errors/process-test-fn-error.js b/src/errors/process-test-fn-error.js
index 70bac3fe1..1f7a85ee8 100644
--- a/src/errors/process-test-fn-error.js
+++ b/src/errors/process-test-fn-error.js
@@ -10,6 +10,10 @@ import {
     UncaughtNonErrorObjectInTestCode,
     ExternalAssertionLibraryError,
 } from './test-run';
+import debug from 'debug';
+
+const debugLog = debug('testcafe:errors');
+
 
 function isAssertionErrorCallsiteFrame (frame) {
     const filename = frame.getFileName();
@@ -22,6 +26,8 @@ function isAssertionErrorCallsiteFrame (frame) {
 }
 
 export default function processTestFnError (err) {
+    debugLog('processTestFnError: %O', err);
+
     if (err && (err.isTestCafeError || err instanceof TestCafeErrorList))
         return err;
 
diff --git a/test/server/reporter-test.js b/test/server/reporter-test.js
index 74bb9bd5f..12c3fe35e 100644
--- a/test/server/reporter-test.js
+++ b/test/server/reporter-test.js
@@ -9,6 +9,7 @@ const Videos                          = require('../../lib/video-recorder/videos
 const delay                           = require('../../lib/utils/delay');
 const { ReporterPluginError }         = require('../../lib/errors/runtime');
 const WarningLog                      = require('../../lib/notifications/warning-log');
+const util                            = require('util');
 
 
 describe('Reporter', () => {
@@ -1402,4 +1403,30 @@ describe('Reporter', () => {
             expect(lastErr.message).startsWith(`The "${method}" method of the "customReporter" reporter produced an uncaught error. Error details:\nError: oops`);
         }
     });
+
+    it('More tolerant error handling for reporter plugin methods', async () => {
+        const taskMock = new TaskMock();
+        const reporter = new Reporter({
+            [ReporterPluginMethod.reportTaskStart]: (error) => {
+                throw error;
+            },
+        }, taskMock._messageBus, null, 'customReporter');
+
+        const errs = [
+            void 0,
+            null,
+            { size: 'big' },
+            new Error('oops'),
+        ];
+
+        for (const err of errs) {
+            try {
+                await reporter.dispatchToPlugin({ method: ReporterPluginMethod.reportTaskStart, args: [err] });
+            }
+            catch (e) {
+                expect(e).instanceOf(ReporterPluginError);
+                expect(e.stack).contains(err?.stack || util.inspect(err));
+            }
+        }
+    });
 });