Repository: DevExpress/testcafe (js)

Issue:
Creating videos creates memory-leak warning on the 11th test in testcafe docker image testcafe:1.20.0
### What is your Scenario?

Running multiple tests and recording videos for each

### What is the Current behavior?

the 11th test throws a MaxListenersExceededWarning

(node:19) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 Page.screencastFrame listeners added to [Chrome]. Use emitter.setMaxListeners() to increase limit
(Use `node --trace-warnings ...` to show where the warning was created)

### What is the Expected behavior?

there should be no MaxListenersExceededWarning, after any number of tests

as of docker image version 1.19.0 no warning was thrown, so something might have changed in the amount of Page.screencastFrame listener that are registered

### What is your public website URL? (or attach your complete example)

https://github.com/GeorgZentgraf/testcafe-bug

### What is your TestCafe test code?

see [test.spec.ts](https://github.com/GeorgZentgraf/testcafe-bug/blob/main/test.spec.ts) 

### Your complete configuration file

[.testcaferc.json](https://github.com/GeorgZentgraf/testcafe-bug/blob/main/.testcaferc.json)

### Your complete test report

docker run test
 Running tests in:
 - Chrome 103.0.5060.114 / Alpine Linux 3.17_alpha20220715

 Getting Started
 ✓ 1
 ✓ 2
 ✓ 3
 ✓ 4
 ✓ 5
 ✓ 6
 ✓ 7
 ✓ 8
 ✓ 9
 ✓ 10
(node:19) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 Page.screencastFrame listeners added to [Chrome]. Use emitter.setMaxListeners() to increase limit
(Use `node --trace-warnings ...` to show where the warning was created)
 ✓ 11
 ✓ 12

### Screenshots

_No response_

### Steps to Reproduce

1. (install docker desktop)
2. clone [repo](https://github.com/GeorgZentgraf/testcafe-bug)
4. run `docker build . -t test`
5. run `docker run test`


### TestCafe version

docker image testcafe/testcafe:1.20.0

### Node.js version

_No response_

### Command-line arguments

default from testcafe docker image

### Browser name(s) and version(s)

Chrome 103.0.5060.114

### Platform(s) and version(s)

_No response_

### Other

The warning doen't show, when using the testcafe image version 1.19.0
Might be related to #7047 

Pull request:
fix warning for high concurrency level (closes #7188)
Closes [#7188](https://github.com/DevExpress/testcafe/issues/7188).

Test on the farm will be very unstable due to high concurrency level (>= 7). 

Patch:
diff --git a/src/cli/argument-parser/index.ts b/src/cli/argument-parser/index.ts
index 87097a13d..97a003722 100644
--- a/src/cli/argument-parser/index.ts
+++ b/src/cli/argument-parser/index.ts
@@ -37,7 +37,7 @@ import getTestcafeVersion from '../../utils/get-testcafe-version';
 import { parsePortNumber, parseList } from './parse-utils';
 import COMMAND_NAMES from './command-names';
 import { SendReportState } from '../../dashboard/interfaces';
-import SKIP_JS_ERRORS_OPTION_NAMES from '../../configuration/skip-js-errors-option-names';
+import { SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES } from '../../configuration/skip-js-errors-option-names';
 
 const REMOTE_ALIAS_RE = /^remote(?::(\d*))?$/;
 
@@ -465,7 +465,7 @@ export default class CLIArgumentParser {
 
     public async parse (argv: string[]): Promise<void> {
         CLIArgumentParser._prepareBooleanOrObjectOption(argv, ['-q', '--quarantine-mode'], Object.values(QUARANTINE_OPTION_NAMES));
-        CLIArgumentParser._prepareBooleanOrObjectOption(argv, ['-e', '--skip-js-errors'], Object.values(SKIP_JS_ERRORS_OPTION_NAMES));
+        CLIArgumentParser._prepareBooleanOrObjectOption(argv, ['-e', '--skip-js-errors'], Object.values(SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES));
 
         const { args, v8Flags } = extractNodeProcessArguments(argv);
 
diff --git a/src/configuration/skip-js-errors-option-names.ts b/src/configuration/skip-js-errors-option-names.ts
index aa1d85726..bbadc2dc2 100644
--- a/src/configuration/skip-js-errors-option-names.ts
+++ b/src/configuration/skip-js-errors-option-names.ts
@@ -1,7 +1,10 @@
-enum SKIP_JS_ERRORS_OPTION_NAMES {
+export enum SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES {
     message = 'message',
     stack = 'stack',
     pageUrl = 'pageUrl',
 }
 
-export default SKIP_JS_ERRORS_OPTION_NAMES;
+export enum SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES {
+    fn = 'fn',
+    dependencies = 'dependencies',
+}
diff --git a/src/errors/runtime/index.js b/src/errors/runtime/index.js
index 5c087b828..bd1937aff 100644
--- a/src/errors/runtime/index.js
+++ b/src/errors/runtime/index.js
@@ -164,3 +164,9 @@ export class RequestRuntimeError extends APIError {
         super(methodName, code, ...args);
     }
 }
+
+export class SkipJsErrorsArgumentApiError extends APIError {
+    constructor (code, ...args) {
+        super('skipJsErrors', code, ...args);
+    }
+}
diff --git a/src/errors/runtime/templates.js b/src/errors/runtime/templates.js
index befde66ae..185ad38c9 100644
--- a/src/errors/runtime/templates.js
+++ b/src/errors/runtime/templates.js
@@ -6,7 +6,7 @@
 import { RUNTIME_ERRORS } from '../types';
 import BrowserConnectionErrorHint from '../../browser/connection/error-hints';
 import { getConcatenatedValuesString } from '../../utils/string';
-import SKIP_JS_ERRORS_OPTION_NAMES from '../../configuration/skip-js-errors-option-names';
+import { SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES, SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES } from '../../configuration/skip-js-errors-option-names';
 
 const DOCUMENTATION_LINKS = {
     TEST_SOURCE_PARAMETER: 'https://testcafe.io/documentation/402639/reference/command-line-interface#file-pathglob-pattern',
@@ -129,12 +129,13 @@ export default {
     [RUNTIME_ERRORS.browserConnectionError]:               '{originErrorMessage}\n{numOfNotOpenedConnection} of {numOfAllConnections} browser connections have not been established:\n{listOfNotOpenedConnections}\n\nHints:\n{listOfHints}',
     [BrowserConnectionErrorHint.TooHighConcurrencyFactor]: 'The host machine may not be powerful enough to handle the specified concurrency factor ({concurrencyFactor}). ' +
                                                            'Try to decrease the concurrency factor or allocate more computing resources to the host machine.',
-    [BrowserConnectionErrorHint.UseBrowserInitOption]:    'Increase the value of the "browserInitTimeout" option if it is too low (currently: {browserInitTimeoutMsg}). This option determines how long TestCafe waits for browsers to be ready.',
-    [BrowserConnectionErrorHint.RestErrorCauses]:         'The error can also be caused by network issues or remote device failure. Make sure that your network connection is stable and you can reach the remote device.',
-    [RUNTIME_ERRORS.cannotFindTestcafeConfigurationFile]: 'Cannot locate a TestCafe configuration file at {filePath}. Either the file does not exist, or the path is invalid.',
-    [RUNTIME_ERRORS.dashboardTokenInJSON]:                'Insecure token declaration: cannot declare a Dashboard token in a JSON configuration file. Use a JavaScript configuration file, or declare a Dashboard token with one of the following: the CLI, the Test Runner API, the TESTCAFE_DASHBOARD_TOKEN environment variable.',
-    [RUNTIME_ERRORS.relativeBaseUrl]:                     'The value of the baseUrl argument cannot be relative: "{baseUrl}"',
-    [RUNTIME_ERRORS.requestUrlInvalidValueError]:         'Requested url isn\'t valid ({actualValue}).',
-    [RUNTIME_ERRORS.requestRuntimeError]:                 'The request was interrupted by an error:\n{message}',
-    [RUNTIME_ERRORS.invalidSkipJsErrorsOption]:           `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_OPTION_NAMES))}`,
+    [BrowserConnectionErrorHint.UseBrowserInitOption]:               'Increase the value of the "browserInitTimeout" option if it is too low (currently: {browserInitTimeoutMsg}). This option determines how long TestCafe waits for browsers to be ready.',
+    [BrowserConnectionErrorHint.RestErrorCauses]:                    'The error can also be caused by network issues or remote device failure. Make sure that your network connection is stable and you can reach the remote device.',
+    [RUNTIME_ERRORS.cannotFindTestcafeConfigurationFile]:            'Cannot locate a TestCafe configuration file at {filePath}. Either the file does not exist, or the path is invalid.',
+    [RUNTIME_ERRORS.dashboardTokenInJSON]:                           'Insecure token declaration: cannot declare a Dashboard token in a JSON configuration file. Use a JavaScript configuration file, or declare a Dashboard token with one of the following: the CLI, the Test Runner API, the TESTCAFE_DASHBOARD_TOKEN environment variable.',
+    [RUNTIME_ERRORS.relativeBaseUrl]:                                'The value of the baseUrl argument cannot be relative: "{baseUrl}"',
+    [RUNTIME_ERRORS.requestUrlInvalidValueError]:                    'Requested url isn\'t valid ({actualValue}).',
+    [RUNTIME_ERRORS.requestRuntimeError]:                            'The request was interrupted by an error:\n{message}',
+    [RUNTIME_ERRORS.invalidSkipJsErrorsOptionsObjectProperty]:       `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES))}.`,
+    [RUNTIME_ERRORS.invalidSkipJsErrorsCallbackWithOptionsProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors callback: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES))}.`,
 };
diff --git a/src/errors/types.js b/src/errors/types.js
index ad538a62b..9cf29eaab 100644
--- a/src/errors/types.js
+++ b/src/errors/types.js
@@ -99,7 +99,7 @@ export const TEST_RUN_ERRORS = {
     actionUrlArgumentError:                                'E96',
     actionStringOrRegexOptionError:                        'E97',
     actionSkipJsErrorsArgumentError:                       'E98',
-    actionSkipJsErrorsDependenciesArgumentError:           'E99',
+    actionFunctionOptionError:                             'E99',
 };
 
 export const RUNTIME_ERRORS = {
@@ -177,5 +177,6 @@ export const RUNTIME_ERRORS = {
     requestRuntimeError:                                'E1073',
     requestCannotResolveTestRun:                        'E1074',
     relativeBaseUrl:                                    'E1075',
-    invalidSkipJsErrorsOption:                          'E1076',
+    invalidSkipJsErrorsOptionsObjectProperty:           'E1076',
+    invalidSkipJsErrorsCallbackWithOptionsProperty:     'E1077',
 };
diff --git a/src/runner/browser-set.ts b/src/runner/browser-set.ts
index 6c1ff0939..60a12ff81 100644
--- a/src/runner/browser-set.ts
+++ b/src/runner/browser-set.ts
@@ -17,6 +17,8 @@ import { createList } from '../utils/string';
 
 const RELEASE_TIMEOUT = 10000;
 
+const COUNT_OWN_AND_OUTER_LISTENERS = 3;
+
 export default class BrowserSet extends EventEmitter {
     private readonly _browserConnections: BrowserConnection[];
     private readonly _browserErrorHandler: (error: Error) => void;
@@ -39,6 +41,7 @@ export default class BrowserSet extends EventEmitter {
         // NOTE: We're setting an empty error handler, because Node kills the process on an 'error' event
         // if there is no handler. See: https://nodejs.org/api/events.html#events_class_events_eventemitter
         this.on('error', noop);
+        this.setMaxListeners(COUNT_OWN_AND_OUTER_LISTENERS + this._browserConnections.length);
     }
 
     private static async _waitIdle (bc: BrowserConnection): Promise<void> {
diff --git a/src/runner/index.js b/src/runner/index.js
index 7d148e2d9..ba438ac40 100644
--- a/src/runner/index.js
+++ b/src/runner/index.js
@@ -51,8 +51,7 @@ import { validateQuarantineOptions } from '../utils/get-options/quarantine';
 import logEntry from '../utils/log-entry';
 import MessageBus from '../utils/message-bus';
 import getEnvOptions from '../dashboard/get-env-options';
-import { isSkipJsErrorsOptionsObject } from '../api/skip-js-errors';
-import { validateSkipJsErrorsOptionsObject } from '../utils/get-options/skip-js-errors';
+import { validateSkipJsErrorsOptionValue } from '../utils/get-options/skip-js-errors';
 
 const DEBUG_LOGGER            = debug('testcafe:runner');
 const DASHBOARD_REPORTER_NAME = 'dashboard';
@@ -313,8 +312,7 @@ export default class Runner extends EventEmitter {
         if (!skipJsErrorsOptions)
             return;
 
-        if (isSkipJsErrorsOptionsObject(skipJsErrorsOptions))
-            validateSkipJsErrorsOptionsObject(skipJsErrorsOptions, GeneralError);
+        validateSkipJsErrorsOptionValue(skipJsErrorsOptions, GeneralError);
     }
 
     async _validateBrowsers () {
diff --git a/src/shared/errors/index.js b/src/shared/errors/index.js
index ce32c6861..9119d31f9 100644
--- a/src/shared/errors/index.js
+++ b/src/shared/errors/index.js
@@ -183,6 +183,11 @@ export class ActionObjectOptionError extends ActionOptionErrorBase {
     }
 }
 
+export class ActionFunctionOptionError extends ActionOptionErrorBase {
+    constructor (optionName, actualValue) {
+        super(TEST_RUN_ERRORS.actionFunctionOptionError, optionName, actualValue);
+    }
+}
 
 // Action execution errors
 //--------------------------------------------------------------------
diff --git a/src/utils/get-options/skip-js-errors.ts b/src/utils/get-options/skip-js-errors.ts
index 2da09ef79..0c085d6bc 100644
--- a/src/utils/get-options/skip-js-errors.ts
+++ b/src/utils/get-options/skip-js-errors.ts
@@ -1,18 +1,18 @@
 import baseGetOptions from './base';
-import SKIP_JS_ERRORS_OPTION_NAMES from '../../configuration/skip-js-errors-option-names';
+import { SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES, SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES } from '../../configuration/skip-js-errors-option-names';
 import { RUNTIME_ERRORS } from '../../errors/types';
 import { GeneralError } from '../../errors/runtime';
 import { Dictionary } from '../../configuration/interfaces';
+import { isSkipJsErrorsCallbackWithOptionsObject, isSkipJsErrorsOptionsObject } from '../../api/skip-js-errors';
 
-function _isSkipJsOption (option: string): option is SKIP_JS_ERRORS_OPTION_NAMES {
-    return Object.values(SKIP_JS_ERRORS_OPTION_NAMES).includes(option as SKIP_JS_ERRORS_OPTION_NAMES);
-}
+export function validateSkipJsErrorsOptionValue (options: boolean | Dictionary<unknown> | SkipJsErrorsOptionsObject | SkipJsErrorsCallback | SkipJsErrorsCallbackWithOptionsObject, ErrorCtor: any): void {
+    if (isSkipJsErrorsCallbackWithOptionsObject(options))
+        validateSkipJsErrorsCallbackWithOptionsObject(options, ErrorCtor);
 
-export function validateSkipJsErrorsOptionsObject (options: Dictionary<unknown> | SkipJsErrorsOptionsObject, ErrorCtor: any): void {
-    for (const key in options) {
-        if (!_isSkipJsOption(key))
-            throw new ErrorCtor(RUNTIME_ERRORS.invalidSkipJsErrorsOption, key);
-    }
+    else if (isSkipJsErrorsOptionsObject(options))
+        validateSkipJsErrorsOptionsObject(options, ErrorCtor);
+
+    return void 0;
 }
 
 export async function getSkipJsErrorsOptions (optionName: string, options: string | boolean | Dictionary<string | RegExp>): Promise<Dictionary<RegExp|string> | boolean> {
@@ -32,3 +32,25 @@ export async function getSkipJsErrorsOptions (optionName: string, options: strin
 
     return parsedOptions;
 }
+
+function _isSkipJsErrorsOptionsObjectOption (option: string): option is SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES {
+    return Object.values(SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES).includes(option as SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES);
+}
+
+function _isSkipJsErrorsCallbackWithOptionsOption (option: string): option is SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES {
+    return Object.values(SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES).includes(option as SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES);
+}
+
+function validateSkipJsErrorsOptionsObject (options: Dictionary<unknown> | SkipJsErrorsOptionsObject, ErrorCtor: any): void {
+    for (const key in options) {
+        if (!_isSkipJsErrorsOptionsObjectOption(key))
+            throw new ErrorCtor(RUNTIME_ERRORS.invalidSkipJsErrorsOptionsObjectProperty, key);
+    }
+}
+
+function validateSkipJsErrorsCallbackWithOptionsObject (options: Dictionary<unknown> | SkipJsErrorsCallbackWithOptionsObject, ErrorCtor: any): void {
+    for (const key in options) {
+        if (!_isSkipJsErrorsCallbackWithOptionsOption(key))
+            throw new ErrorCtor(RUNTIME_ERRORS.invalidSkipJsErrorsCallbackWithOptionsProperty, key);
+    }
+}


Test patch:
diff --git a/src/api/structure/testing-unit.ts b/src/api/structure/testing-unit.ts
index d7dcbb942..030140061 100644
--- a/src/api/structure/testing-unit.ts
+++ b/src/api/structure/testing-unit.ts
@@ -18,9 +18,8 @@ import {
     Dictionary, SkipJsErrorsCallback, SkipJsErrorsCallbackWithOptionsObject,
 } from '../../configuration/interfaces';
 import { dirname } from 'path';
-import { isSkipJsErrorsOptionsObject } from '../skip-js-errors';
-import { validateSkipJsErrorsOptionsObject } from '../../utils/get-options/skip-js-errors';
-import { APIError } from '../../errors/runtime';
+import { validateSkipJsErrorsOptionValue } from '../../utils/get-options/skip-js-errors';
+import { SkipJsErrorsArgumentApiError } from '../../errors/runtime';
 
 export default abstract class TestingUnit extends BaseUnit {
     public readonly testFile: TestFile;
@@ -114,8 +113,7 @@ export default abstract class TestingUnit extends BaseUnit {
 
         this.skipJsErrorsOptions = options;
 
-        if (isSkipJsErrorsOptionsObject(this.skipJsErrorsOptions))
-            validateSkipJsErrorsOptionsObject(this.skipJsErrorsOptions, APIError);
+        validateSkipJsErrorsOptionValue(this.skipJsErrorsOptions, SkipJsErrorsArgumentApiError);
 
         return this.apiOrigin;
     }
diff --git a/src/errors/test-run/index.js b/src/errors/test-run/index.js
index 259bda8aa..76ad58a61 100644
--- a/src/errors/test-run/index.js
+++ b/src/errors/test-run/index.js
@@ -285,16 +285,6 @@ export class ActionSkipJsErrorsArgumentTypeError extends TestRunErrorBase {
     }
 }
 
-export class ActionSkipJsErrorsDependenciesArgumentTypeError extends TestRunErrorBase {
-    constructor (argumentName, actualValue) {
-        super(TEST_RUN_ERRORS.actionSkipJsErrorsDependenciesArgumentError);
-
-        this.argumentName = argumentName;
-        this.actualValue  = actualValue;
-    }
-}
-
-
 // Action execution errors
 //--------------------------------------------------------------------
 export class UncaughtErrorInCustomScript extends TestRunErrorBase {
diff --git a/src/errors/test-run/templates.js b/src/errors/test-run/templates.js
index 9068797bb..8907c8063 100644
--- a/src/errors/test-run/templates.js
+++ b/src/errors/test-run/templates.js
@@ -448,7 +448,7 @@ export default {
         Cannot execute the skipJsErrors method. The value of the "${err.argumentName}" argument belongs to an unsupported type (${err.actualValue}). The "${err.argumentName}" supports the following data types: Boolean, Object, Function.
     `,
 
-    [TEST_RUN_ERRORS.actionSkipJsErrorsDependenciesArgumentError]: err => `
-        Cannot execute the skipJsErrors method. The value of the "dependencies" argument belongs to an unsupported type (${err.actualValue}). The "dependencies" argument only supports the Object data type.
+    [TEST_RUN_ERRORS.actionFunctionOptionError]: err => `
+        The value of the "${err.optionName}" option belongs to an unsupported data type (${err.actualValue}). The "${err.optionName}" option only accepts function types values.
     `,
 };
diff --git a/src/test-run/commands/actions.js b/src/test-run/commands/actions.js
index dcf3f2cce..bd3b8651e 100644
--- a/src/test-run/commands/actions.js
+++ b/src/test-run/commands/actions.js
@@ -15,6 +15,7 @@ import {
     GetProxyUrlOptions,
     RequestOptions,
     SkipJsErrorsOptions,
+    SkipJsErrorsCallbackWithOptions,
 } from './options';
 
 import {
@@ -48,7 +49,11 @@ import {
 import { SetNativeDialogHandlerCodeWrongTypeError } from '../../errors/test-run';
 import { ExecuteClientFunctionCommand } from './observation';
 import { camelCase } from 'lodash';
-import { prepareSkipJsErrorsOptions, isSkipJsErrorsOptionsObject } from '../../api/skip-js-errors';
+import {
+    prepareSkipJsErrorsOptions,
+    isSkipJsErrorsOptionsObject,
+    isSkipJsErrorsCallbackWithOptionsObject,
+} from '../../api/skip-js-errors';
 
 
 // Initializers
@@ -127,7 +132,10 @@ function initSkipJsErrorsOptions (name, val, initOptions, validate = true) {
     if (val === void 0)
         return true;
 
-    if (isSkipJsErrorsOptionsObject(val))
+    if (isSkipJsErrorsCallbackWithOptionsObject(val))
+        val = new SkipJsErrorsCallbackWithOptions(val, validate);
+
+    else if (isSkipJsErrorsOptionsObject(val))
         val = new SkipJsErrorsOptions(val, validate);
 
     return prepareSkipJsErrorsOptions(val);
diff --git a/src/test-run/commands/options.d.ts b/src/test-run/commands/options.d.ts
index 5dcdb6361..0df3e91ce 100644
--- a/src/test-run/commands/options.d.ts
+++ b/src/test-run/commands/options.d.ts
@@ -122,3 +122,9 @@ export class SkipJsErrorsOptions {
     public message?: string | RegExp;
     public pageUrl?: string | RegExp;
 }
+
+export class SkipJsErrorsCallbackWithOptions {
+    public constructor (obj: object, validate: boolean);
+    public fn: SkipJsErrorsCallback;
+    public dependencies: { [key: string]: any }
+}
diff --git a/src/test-run/commands/options.js b/src/test-run/commands/options.js
index 7ec5f5d19..d575fb654 100644
--- a/src/test-run/commands/options.js
+++ b/src/test-run/commands/options.js
@@ -16,6 +16,7 @@ import {
     createUrlSearchParamsValidator,
     createObjectValidator,
     createStringOrRegexValidator,
+    createFunctionValidator,
 } from './validations/factories';
 import {
     ActionIntegerOptionError,
@@ -29,6 +30,7 @@ import {
     ActionUrlSearchParamsOptionError,
     ActionObjectOptionError,
     ActionStringOrRegexOptionError,
+    ActionFunctionOptionError,
 } from '../../shared/errors';
 
 export const integerOption         = createIntegerValidator(ActionIntegerOptionError);
@@ -42,7 +44,7 @@ export const numberOption          = createNumberValidator(ActionNumberOptionErr
 export const urlOption             = createUrlValidator(ActionUrlOptionError);
 export const urlSearchParamsOption = createUrlSearchParamsValidator(ActionUrlSearchParamsOptionError);
 export const objectOption          = createObjectValidator(ActionObjectOptionError);
-
+export const functionOption        = createFunctionValidator(ActionFunctionOptionError);
 
 // Actions
 export class ActionOptions extends Assignable {
@@ -402,6 +404,20 @@ export class SkipJsErrorsOptions extends Assignable {
         ];
     }
 }
+export class SkipJsErrorsCallbackWithOptions extends Assignable {
+    constructor (obj, validate) {
+        super();
+
+        this._assignFrom(obj, validate);
+    }
+
+    _getAssignableProperties () {
+        return [
+            { name: 'fn', type: functionOption, required: true },
+            { name: 'dependencies', type: objectOption, required: false },
+        ];
+    }
+}
 
 // Initializers
 function initRequestAuthOption (name, val, initOptions, validate = true) {
diff --git a/src/test-run/commands/validations/argument.js b/src/test-run/commands/validations/argument.js
index fdd6cc8e0..2f2b8b533 100644
--- a/src/test-run/commands/validations/argument.js
+++ b/src/test-run/commands/validations/argument.js
@@ -29,15 +29,12 @@ import {
     ActionRequiredCookieArguments,
     ActionUrlArgumentError,
     ActionSkipJsErrorsArgumentTypeError,
-    ActionSkipJsErrorsDependenciesArgumentTypeError,
 } from '../../../errors/test-run';
 
 import { URL } from 'url';
 import { assertPageUrl } from '../../../api/test-page-url';
 import checkFilePath from '../../../utils/check-file-path';
 import { castArray } from 'lodash';
-import { isSkipJsErrorsCallbackWithOptionsObject } from '../../../api/skip-js-errors';
-
 
 // Validators
 export const integerArgument         = createIntegerValidator(ActionIntegerArgumentError);
@@ -195,7 +192,4 @@ export function skipJsErrorOptions (name, val) {
 
     if (valType !== 'undefined' && valType !== 'object' && valType !== 'boolean' && valType !== 'function')
         throw new ActionSkipJsErrorsArgumentTypeError(name, valType);
-
-    if (isSkipJsErrorsCallbackWithOptionsObject(val) && val.dependencies && typeof val.dependencies !== 'object')
-        throw new ActionSkipJsErrorsDependenciesArgumentTypeError('dependencies', typeof dependencies);
 }
diff --git a/src/test-run/commands/validations/factories.js b/src/test-run/commands/validations/factories.js
index 1b525f6da..1a9f87cda 100644
--- a/src/test-run/commands/validations/factories.js
+++ b/src/test-run/commands/validations/factories.js
@@ -108,3 +108,12 @@ export function createObjectValidator (ErrorCtor) {
             throw new ErrorCtor(name, valType);
     };
 }
+
+export function createFunctionValidator (ErrorCtor) {
+    return (name, val) => {
+        const valType = typeof val;
+
+        if (valType !== 'function')
+            throw new ErrorCtor(name, valType);
+    };
+}
diff --git a/test/server/api-test.js b/test/server/api-test.js
index e5c0a3cac..108f2d6e2 100644
--- a/test/server/api-test.js
+++ b/test/server/api-test.js
@@ -425,6 +425,81 @@ describe('API', function () {
                 expect(err.code).eql(code);
             }
         });
+
+        it('Should raise an error if "fixture.skipJsErrors" method argument has invalid value type', () => {
+            const testfile = resolve('test/server/data/test-suites/skip-js-errors/fixture-invalid-argument.js');
+
+            return compile(testfile)
+                .then(() => {
+                    throw new Error('Promise rejection expected');
+                })
+                .catch(err => {
+                    assertAPIError(err, {
+                        stackTop: testfile,
+
+                        message: 'Cannot prepare tests due to the following error:\n\n' +
+                                 'The skipJsErrors options argument (string) is not of expected type (boolean, non-null object or a function).',
+
+                        callsite: '   1 |fixture`SkipJsErrors API`\n' +
+                                  ' > 2 |    .skipJsErrors(\'test\');\n' +
+                                  '   3 |\n' +
+                                  '   4 |test(\'test\', () => {\n' +
+                                  '   5 |\n' +
+                                  '   6 |})\n' +
+                                  '   7 |\n',
+                    });
+                });
+        });
+
+        it('Should raise an error if "fixture.skipJsErrors" method argument has invalid SkipJsErrorsOptionsObject structure', () => {
+            const testfile = resolve('test/server/data/test-suites/skip-js-errors/fixture-options-object-argument.js');
+
+            return compile(testfile)
+                .then(() => {
+                    throw new Error('Promise rejection expected');
+                })
+                .catch(err => {
+                    assertAPIError(err, {
+                        stackTop: testfile,
+
+                        message: 'Cannot prepare tests due to the following error:\n\n' +
+                                 'The "invalidProp" option does not exist. Use the following options to configure skipJsErrors: "message", "stack", and "pageUrl".',
+
+                        callsite: '   1 |fixture`SkipJsErrors API`\n' +
+                                  ' > 2 |    .skipJsErrors({ message: \'test\', invalidProp: false });\n' +
+                                  '   3 |\n' +
+                                  '   4 |test(\'test\', () => {\n' +
+                                  '   5 |\n' +
+                                  '   6 |})\n' +
+                                  '   7 |\n',
+                    });
+                });
+        });
+
+        it('Should raise an error if "fixture.skipJsErrors" method argument has invalid SkipJsErrorsCallbackWithOptionsObject structure', () => {
+            const testfile = resolve('test/server/data/test-suites/skip-js-errors/fixture-function-with-options-object-argument.js');
+
+            return compile(testfile)
+                .then(() => {
+                    throw new Error('Promise rejection expected');
+                })
+                .catch(err => {
+                    assertAPIError(err, {
+                        stackTop: testfile,
+
+                        message: 'Cannot prepare tests due to the following error:\n\n' +
+                                 'The "invalidProp" option does not exist. Use the following options to configure skipJsErrors callback: "fn" and "dependencies".',
+
+                        callsite: '   1 |fixture`SkipJsErrors API`\n' +
+                                  ' > 2 |    .skipJsErrors({ fn: () => true, invalidProp: false });\n' +
+                                  '   3 |\n' +
+                                  '   4 |test(\'test\', () => {\n' +
+                                  '   5 |\n' +
+                                  '   6 |})\n' +
+                                  '   7 |\n',
+                    });
+                });
+        });
     });
 
     describe('test', function () {
diff --git a/test/server/data/expected-test-run-errors/action-skip-js-errors-dependencies-argument-error b/test/server/data/expected-test-run-errors/action-function-option-error
similarity index 69%
rename from test/server/data/expected-test-run-errors/action-skip-js-errors-dependencies-argument-error
rename to test/server/data/expected-test-run-errors/action-function-option-error
index 248b0322f..009f1cac4 100644
--- a/test/server/data/expected-test-run-errors/action-skip-js-errors-dependencies-argument-error
+++ b/test/server/data/expected-test-run-errors/action-function-option-error
@@ -1,4 +1,4 @@
-Cannot execute the skipJsErrors method. The value of the "dependencies" argument belongs to an unsupported type (string). The "dependencies" argument only supports the Object data type.
+The value of the "fn" option belongs to an unsupported data type (string). The "fn" option only accepts function types values.
 
 Browser: Chrome 15.0.874.120 / macOS 10.15
 Screenshot: /unix/path/with/<tag>
diff --git a/test/server/data/test-suites/skip-js-errors/fixture-function-with-options-object-argument.js b/test/server/data/test-suites/skip-js-errors/fixture-function-with-options-object-argument.js
new file mode 100644
index 000000000..924085dcf
--- /dev/null
+++ b/test/server/data/test-suites/skip-js-errors/fixture-function-with-options-object-argument.js
@@ -0,0 +1,6 @@
+fixture`SkipJsErrors API`
+    .skipJsErrors({ fn: () => true, invalidProp: false });
+
+test('test', () => {
+
+})
diff --git a/test/server/data/test-suites/skip-js-errors/fixture-invalid-argument.js b/test/server/data/test-suites/skip-js-errors/fixture-invalid-argument.js
new file mode 100644
index 000000000..c56ae8ffe
--- /dev/null
+++ b/test/server/data/test-suites/skip-js-errors/fixture-invalid-argument.js
@@ -0,0 +1,6 @@
+fixture`SkipJsErrors API`
+    .skipJsErrors('test');
+
+test('test', () => {
+
+})
diff --git a/test/server/data/test-suites/skip-js-errors/fixture-options-object-argument.js b/test/server/data/test-suites/skip-js-errors/fixture-options-object-argument.js
new file mode 100644
index 000000000..7d961ab9a
--- /dev/null
+++ b/test/server/data/test-suites/skip-js-errors/fixture-options-object-argument.js
@@ -0,0 +1,6 @@
+fixture`SkipJsErrors API`
+    .skipJsErrors({ message: 'test', invalidProp: false });
+
+test('test', () => {
+
+})
diff --git a/test/server/test-run-error-formatting-test.js b/test/server/test-run-error-formatting-test.js
index 45cea3fee..0542df509 100644
--- a/test/server/test-run-error-formatting-test.js
+++ b/test/server/test-run-error-formatting-test.js
@@ -102,9 +102,8 @@ const {
     ActionObjectOptionError,
     ActionUrlArgumentError,
     ActionSkipJsErrorsArgumentTypeError,
-    ActionSkipJsErrorsDependenciesArgumentTypeError,
-}                                             = require('../../lib/errors/test-run');
-const { ActionStringOrRegexOptionError }      = require('../../lib/shared/errors');
+}                                                                   = require('../../lib/errors/test-run');
+const { ActionStringOrRegexOptionError, ActionFunctionOptionError } = require('../../lib/shared/errors');
 
 const untestedErrorTypes = Object.keys(TEST_RUN_ERRORS).map(key => TEST_RUN_ERRORS[key]);
 
@@ -826,8 +825,8 @@ describe('Error formatting', () => {
             assertErrorMessage('action-skip-js-errors-argument-error', new ActionSkipJsErrorsArgumentTypeError('options', 'string'));
         });
 
-        it('Should format "actionSkipJsErrorsDependenciesArgumentTypeError"', () => {
-            assertErrorMessage('action-skip-js-errors-dependencies-argument-error', new ActionSkipJsErrorsDependenciesArgumentTypeError('dependencies', 'string'));
+        it('Should format "actionFunctionOptionError"', () => {
+            assertErrorMessage('action-function-option-error', new ActionFunctionOptionError('fn', 'string'));
         });
     });