<commit_before>/* eslint-disable max-lines-per-function */

'use strict';

/*!
 * V4Fire Client Core
 * https://github.com/V4Fire/Client
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Client/blob/master/LICENSE
 */

const
	os = require('os'),
	path = require('upath'),
	arg = require('arg');

const
	{build, src} = require('config'),
	{resolve} = require('@pzlr/build-core'),
	{wait, getBrowserInstance, getSelectedBrowsers, getBrowserArgs, getTestClientName} = include('build/helpers');

const
	cpus = os.cpus().length,
	START_PORT = 8000;

/**
 * Registers gulp tasks to test the project
 *
 * @example
 * ```bash
 * # Builds an application to test the specified component
 * npx gulp test:component:build --name b-button
 *
 * # Runs tests for the specified component
 * npx gulp test:component:run --name b-button --browsers ff
 *
 * # Builds and runs tests for the specified component
 * npx gulp test:component --name b-button
 *
 * # Builds and runs tests for all components
 * npx gulp test:components -p 16
 * ```
 */
module.exports = function init(gulp = require('gulp')) {
	const
		$ = require('gulp-load-plugins')({scope: ['optionalDependencies']});

	/**
	 * Builds an application to test the specified component.
	 * Arguments:
	 *
	 * * --name - name of the component to test
	 * * [--client-output] - directory to save the generated code
	 * * [--cache-type='memory'] - WebPack cache type
	 * * [--public-path] - WebPack publicPath value
	 * * [--es='ES2019'] - version of the used ECMAScript specification to generate
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:build --name b-button --client-output b-button --watch
	 * ```
	 *
	 * If you prefer to create components at runtime, you can use a dummy component.
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:build --name b-dummy
	 * ```
	 */
	gulp.task('test:component:build', () => {
		const args = arg({
			'--suit': String,
			'--client-name': String,
			'--runtime-render': String
		}, {permissive: true});

		const
			isRuntimeRender = args['--runtime-render'] ? JSON.parse(args['--runtime-render']) : false;

		try {
			const name = arg({'--name': String}, {permissive: true})['--name'];
			args['--name'] = name;
		} catch {}

		if (isRuntimeRender) {
			args['--name'] = 'b-dummy';
		}

		if (!args['--name'] && !isRuntimeRender) {
			throw new ReferenceError('"--name" parameter is not specified');
		}

		const
			suitArg = args['--suit'] ? `--suit ${args['--suit']}` : '',
			extraArgs = args._.slice(1).join(' ');

		const argsString = [
			['--client-output', args['--client-name'] || args['--name']],
			['--components', args['--name']],
			['--cache-type', 'memory'],
			['--public-path', ''],
			['--es', 'ES2019']
		].flat().join(' ');

		return $.run(`npx webpack ${argsString} ${suitArg} ${extraArgs}`, {verbosity: 3})
			.exec()
			.on('error', console.error);
	});

	/**
	 * Runs tests for the specified component.
	 * Arguments:
	 *
	 * * --name - name of the component to test
	 * * [--port] - port to launch the test server
	 * * [--start-port] - starting port for `portfinder`
	 * * [--page='p-v4-components-demo'] - demo page to run tests
	 * * [--browsers] - list of browsers to test (firefox (ff), chromium (chrome), webkit (wk))
	 * * [--device] - name of used device, for instance, "iPhone_11" or "Pixel_2"
	 * * [--close=true] - should or not close the running browsers after finishing the tests
	 * * [--headless=true] - should or not run browsers with the headless option
	 * * [--reinit-browser=false] - should or not reuse already existence browser instances
	 * * [--test-entry] - directory with entry points to build the application
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:run --name b-button --browsers ff,chrome
	 * ```
	 *
	 * If you prefer to create components at runtime, you can use a dummy component.
	 * Make sure that that all components you want to test are declared as dependencies
	 * into `index.js` file of the demo page.
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:run --name b-dummy --test-entry base/b-router/test'
	 * ```
	 */
	gulp.task('test:component:run', async () => {
		const
			pzlr = require('@pzlr/build-core');

		const
			process = require('process'),
			portfinder = require('portfinder');

		const
			http = require('http'),
			nodeStatic = require('node-static');

		const
			fs = require('fs-extra-promise'),
			path = require('upath');

		const
			{devices} = require('playwright');

		const args = arg({
			'--name': String,
			'--port': Number,
			'--start-port': Number,
			'--page': String,
			'--browsers': String,
			'--device': String,
			'--close': String,
			'--headless': String,
			'--client-name': String,
			'--reinit-browser': String,
			'--test-entry': String,
			'--runner': String,
			'--runtime-render': String
		}, {permissive: true});

		const
			isRuntimeRender = args['--runtime-render'] ? JSON.parse(args['--runtime-render']) : false;

		if (isRuntimeRender) {
			args['--name'] = 'b-dummy';
		}

		if (!args['--name']) {
			throw new ReferenceError('"--name" parameter is not specified');
		}

		args['--client-name'] = args['--client-name'] || args['--name'];

		const
			browsers = getSelectedBrowsers();

		let exitCode = 0;

		const cliParams = {
			headless: true,
			'reinit-browser': false,
			close: true
		};

		Object.keys(cliParams).forEach((key) => {
			cliParams[key] = args[`--${key}`] ? JSON.parse(args[`--${key}`]) : cliParams[key];
		});

		// eslint-disable-next-line require-atomic-updates
		args['--port'] = args['--port'] || await portfinder.getPortPromise({
			port: args['--start-port'] || START_PORT
		});

		// eslint-disable-next-line require-atomic-updates
		args['--page'] = args['--page'] || build.demoPage;

		const
			fileServer = new nodeStatic.Server(src.output(args['--client-name']));

		const server = http.createServer((req, res) => {
			req.addListener('end', () => {
				fileServer.serve(req, res);
			}).resume();
		}).listen(args['--port']);

		const
			componentDir = resolve.blockSync(args['--name']),
			tmpDir = path.join(src.cwd(), 'tmp', path.relative(src.src(), componentDir));

		fs.mkdirpSync(tmpDir);

		const
			entryPoint = args['--test-entry'],
			testPath = entryPoint ? resolve.blockSync(entryPoint) : path.join(componentDir, 'test'),
			test = require(testPath);

		const browserParams = {
			chromium: {},
			firefox: {},
			webkit: {}
		};

		const
			browsersPromises = [];

		for (const browserType of browsers) {
			browsersPromises.push(createBrowser(browserType));
		}

		await Promise.all(browsersPromises);

		for (const browserType of browsers) {
			await runTest(browserType);
		}

		await server.close();

		function getTestEnv(browserType) {
			const
				Jasmine = require('jasmine'),
				jasmine = new Jasmine();

			jasmine.configureDefaultReporter({});
			Object.assign(globalThis, jasmine.env);

			globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (10).seconds();

			console.log('\n-------------');
			console.log('Starting to test');

			pzlr.config.dependencies.forEach((dep) => {
				console.log(`${dep} version: ${require(`${dep}/package.json`).version}`);
			});

			console.log(`env component: ${args['--name']}`);
			console.log(`test entry: ${args['--test-entry']}`);
			console.log(`runner: ${args['--runner']}`);
			console.log(`browser: ${browserType}`);
			console.log('-------------\n');

			return jasmine.env;
		}

		async function createBrowser(browserType) {
			const browserInstanceParams = {
				headless: cliParams.headless
			};

			const browserInstanceOptions = {
				reInit: cliParams['reinit-browser']
			};

			const
				browser = await getBrowserInstance(browserType, browserInstanceParams, browserInstanceOptions),
				device = args['--device']?.replace(/_/g, ' ');

			if (device != null && devices[device] == null) {
				throw Error(`The specified devise "${device}" is not supported`);
			}

			const contextOpts = {
				...device ? devices[device] : null
			};

			const
				context = await browser.newContext(contextOpts),
				page = await context.newPage();

			const
				testURL = `localhost:${args['--port']}/${args['--page']}.html`;

			browserParams[browserType] = {
				testURL,
				browser,
				browserType,
				page,
				context,
				componentDir,
				tmpDir
			};
		}

		async function runTest(browserType) {
			const
				params = browserParams[browserType];

			const {
				testURL,
				page
			} = params;

			await page.goto(testURL);
			const testEnv = getTestEnv(browserType);

			const close = () => {
				if (!cliParams['reinit-browser'] && cliParams.close) {
					params.browser.close();
				}

				process.exitCode = exitCode;
			};

			if (await test(page, params) === false) {
				close();
				return;
			}

			testEnv.addReporter({
				specDone: (res) => {
					if (exitCode === 1) {
						return;
					}

					exitCode = res.status === 'failed' ? 1 : 0;
				}
			});

			await new Promise((resolve) => {
				testEnv.afterAll(() => {
					console.log('\n\n\n');
					resolve();
				}, 10e3);

				testEnv.execute();
			}).then(close, close);
		}
	});

	/**
	 * Builds and runs tests for the specified component
	 *
	 * @see test:component:build
	 * @see test:component:run
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component --name b-button --browsers ff,chrome
	 * ```
	 */
	gulp.task('test:component', gulp.series([
		'test:component:build',
		'test:component:run'
	]));

	/**
	 * Builds and runs tests for all components.
	 * Arguments:
	 *
	 * * [--processes] | [-p] - number of available CPUs to build the application and run tests
	 * * [--test-processes] | [-tp] - number of available CPUs to run tests
	 * * [--build-processes] | [-tb] - number of available CPUs to build the application
	 * * [--only-run] - allows run all test cases without the building stage
	 *
	 * @see test:component:run
	 * @see test:component:build
	 *
	 * @example
	 * ```bash
	 * npx gulp test:components -p 16
	 * ```
	 */
	gulp.task('test:components', async (cb) => {
		console.log(`CPUS available: ${cpus}`);

		const
			playwright = require('playwright');

		const
			{cwd} = resolve,
			cases = require(path.join(cwd, 'tests/cases.js'));

		const processesArgs = arg({
			'--processes': Number,
			'--test-processes': Number,
			'--build-processes': Number,

			'-p': '--processes',
			'-tp': '--test-processes',
			'-bp': '--build-processes'
		}, {permissive: true});

		const cliArgs = arg({
			'--reinit-browser': String,
			'--only-run': Boolean,
			'--browser-args': String
		}, {permissive: true});

		const cliParams = {
			reInitBrowser: cliArgs['--reinit-browser'] ? JSON.parse(cliArgs['--reinit-browser']) : false,
			browserArgs: cliArgs['--browser-args'] ? cliArgs['--browser-args'].split(',') : []
		};

		const
			buildProcess = processesArgs['--build-processes'] || processesArgs['--processes'] || cpus,
			testProcess = processesArgs['--test-processes'] || processesArgs['--processes'] || cpus;

		const
			waitForQuotas = (map, maxQueue) => wait(() => map.size < maxQueue),
			waitForEmpty = (map) => wait(() => map.size === 0);

		const exec = (execString, successCb, failCb) => new Promise((res, rej) => {
			$.run(execString, {verbosity: 3})
				.exec('', () => {
					successCb && successCb();
					res();
				})

				.on('error', (err) => {
					failCb && failCb(execString, err);
					rej();
				});
		});

		// Build components

		const
			buildCache = {},
			buildMap = new Map();

		if (!cliArgs['--only-run']) {
			for (let i = 0; i < cases.length; i++) {
				let
					c = cases[i];

				if (!c.includes('--name')) {
					c = `${c} --runtime-render true`;
				}

				const args = arg({
					'--name': String
				}, {argv: c.split(' '), permissive: true});

				args['--client-name'] = getTestClientName(args['--name'], build.suit);

				if (buildCache[args['--client-name']]) {
					continue;
				}

				const argsString = [
					['--suit', build.suit],
					['--name', args['--name']],
					['--client-name', args['--client-name']]
				].flat().join(' ');

				const extraArgs = args._.join(' ');

				await waitForQuotas(buildMap, buildProcess);

				buildMap.set(
					argsString,
					exec(`npx gulp test:component:build ${argsString} ${extraArgs}`, () => buildMap.delete(argsString))
				);

				buildCache[args['--client-name']] = true;
			}
		}

		await waitForEmpty(buildMap);

		// Launch browser server

		let endpointArg = '';

		const wsEndpoints = {
			chromium: '',
			firefox: '',
			webkit: ''
		};

		const
			servers = {},
			browsers = getSelectedBrowsers();

		if (!cliParams.reInitBrowser) {
			for (const browserType of browsers) {
				const
					browser = await playwright[browserType].launchServer({args: getBrowserArgs()}),
					wsEndpoint = browser.wsEndpoint();

				servers[browserType] = browser;
				await playwright[browserType].connect({wsEndpoint});
				wsEndpoints[browserType] = wsEndpoint;
			}

			endpointArg = Object.entries(wsEndpoints).map(([key, value]) => `--${key}WsEndpoint ${value}`).join(' ');
		}

		// Run tests

		const
			totalCases = [],
			failedCases = [],
			testMap = new Map();

		for (let i = 0; i < cases.length; i++) {
			let
				c = cases[i];

			if (!c.includes('--name')) {
				c = `${c} --runtime-render true`;
			}

			// Set the beginning of the searching range for a free port
			c = `${c} --start-port ${START_PORT + i * 100}`;

			const args = arg({
				'--name': String
			}, {argv: c.split(' '), permissive: true});

			args['--client-name'] = getTestClientName(args['--name'], build.suit);

			const
				browserArgs = cliParams.browserArgs.join(','),
				argsString = `${c} --client-name ${args['--client-name']} --browsers ${browsers.join(',')} --browser-args ${browserArgs}`,
				extraArgs = args._.join(' ');

			totalCases.push(argsString);
			await waitForQuotas(testMap, testProcess);

			const
				onTestEnd = (argsString) => testMap.delete(argsString);

			testMap.set(
				argsString,
				exec(`npx gulp test:component:run ${argsString} ${endpointArg} ${extraArgs}`,
					() => onTestEnd(argsString),
					() => {
						onTestEnd(argsString);
						failedCases.push(argsString);
					})
			);
		}

		await waitForEmpty(testMap);

		console.log('\n-------------');
		console.log(`\n✔ Tests passed: ${totalCases.filter((v) => !failedCases.includes(v)).length}`);
		console.log(`\n❌ Tests failed: ${failedCases.length}`);

		if (failedCases.length) {
			console.log('\n❗ Failed tests:');
			console.log(`\n${failedCases.join('\n')}`);
		}

		console.log('\n-------------\n');

		if (!cliParams.reInitBrowser) {
			Object.keys(servers).forEach((key) => servers[key].close());
		}

		if (failedCases.length) {
			throw new Error('Tests wasn\'t passed');
		}

		cb();
	});
};
<commit_msg>Added loging of a webpack version<commit_after>/* eslint-disable max-lines-per-function */

'use strict';

/*!
 * V4Fire Client Core
 * https://github.com/V4Fire/Client
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Client/blob/master/LICENSE
 */

const
	os = require('os'),
	path = require('upath'),
	arg = require('arg');

const
	{build, src} = require('config'),
	{resolve} = require('@pzlr/build-core'),
	{wait, getBrowserInstance, getSelectedBrowsers, getBrowserArgs, getTestClientName} = include('build/helpers');

const
	cpus = os.cpus().length,
	START_PORT = 8000;

/**
 * Registers gulp tasks to test the project
 *
 * @example
 * ```bash
 * # Builds an application to test the specified component
 * npx gulp test:component:build --name b-button
 *
 * # Runs tests for the specified component
 * npx gulp test:component:run --name b-button --browsers ff
 *
 * # Builds and runs tests for the specified component
 * npx gulp test:component --name b-button
 *
 * # Builds and runs tests for all components
 * npx gulp test:components -p 16
 * ```
 */
module.exports = function init(gulp = require('gulp')) {
	const
		$ = require('gulp-load-plugins')({scope: ['optionalDependencies']});

	/**
	 * Builds an application to test the specified component.
	 * Arguments:
	 *
	 * * --name - name of the component to test
	 * * [--client-output] - directory to save the generated code
	 * * [--cache-type='memory'] - WebPack cache type
	 * * [--public-path] - WebPack publicPath value
	 * * [--es='ES2019'] - version of the used ECMAScript specification to generate
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:build --name b-button --client-output b-button --watch
	 * ```
	 *
	 * If you prefer to create components at runtime, you can use a dummy component.
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:build --name b-dummy
	 * ```
	 */
	gulp.task('test:component:build', () => {
		const args = arg({
			'--suit': String,
			'--client-name': String,
			'--runtime-render': String
		}, {permissive: true});

		const
			isRuntimeRender = args['--runtime-render'] ? JSON.parse(args['--runtime-render']) : false;

		try {
			const name = arg({'--name': String}, {permissive: true})['--name'];
			args['--name'] = name;
		} catch {}

		if (isRuntimeRender) {
			args['--name'] = 'b-dummy';
		}

		if (!args['--name'] && !isRuntimeRender) {
			throw new ReferenceError('"--name" parameter is not specified');
		}

		const
			suitArg = args['--suit'] ? `--suit ${args['--suit']}` : '',
			extraArgs = args._.slice(1).join(' ');

		const argsString = [
			['--client-output', args['--client-name'] || args['--name']],
			['--components', args['--name']],
			['--cache-type', 'memory'],
			['--public-path', ''],
			['--es', 'ES2019']
		].flat().join(' ');

		console.log(`webpack version: ${require('webpack/package.json').version}`);

		return $.run(`npx webpack ${argsString} ${suitArg} ${extraArgs}`, {verbosity: 3})
			.exec()
			.on('error', console.error);
	});

	/**
	 * Runs tests for the specified component.
	 * Arguments:
	 *
	 * * --name - name of the component to test
	 * * [--port] - port to launch the test server
	 * * [--start-port] - starting port for `portfinder`
	 * * [--page='p-v4-components-demo'] - demo page to run tests
	 * * [--browsers] - list of browsers to test (firefox (ff), chromium (chrome), webkit (wk))
	 * * [--device] - name of used device, for instance, "iPhone_11" or "Pixel_2"
	 * * [--close=true] - should or not close the running browsers after finishing the tests
	 * * [--headless=true] - should or not run browsers with the headless option
	 * * [--reinit-browser=false] - should or not reuse already existence browser instances
	 * * [--test-entry] - directory with entry points to build the application
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:run --name b-button --browsers ff,chrome
	 * ```
	 *
	 * If you prefer to create components at runtime, you can use a dummy component.
	 * Make sure that that all components you want to test are declared as dependencies
	 * into `index.js` file of the demo page.
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component:run --name b-dummy --test-entry base/b-router/test'
	 * ```
	 */
	gulp.task('test:component:run', async () => {
		const
			pzlr = require('@pzlr/build-core');

		const
			process = require('process'),
			portfinder = require('portfinder');

		const
			http = require('http'),
			nodeStatic = require('node-static');

		const
			fs = require('fs-extra-promise'),
			path = require('upath');

		const
			{devices} = require('playwright');

		const args = arg({
			'--name': String,
			'--port': Number,
			'--start-port': Number,
			'--page': String,
			'--browsers': String,
			'--device': String,
			'--close': String,
			'--headless': String,
			'--client-name': String,
			'--reinit-browser': String,
			'--test-entry': String,
			'--runner': String,
			'--runtime-render': String
		}, {permissive: true});

		const
			isRuntimeRender = args['--runtime-render'] ? JSON.parse(args['--runtime-render']) : false;

		if (isRuntimeRender) {
			args['--name'] = 'b-dummy';
		}

		if (!args['--name']) {
			throw new ReferenceError('"--name" parameter is not specified');
		}

		args['--client-name'] = args['--client-name'] || args['--name'];

		const
			browsers = getSelectedBrowsers();

		let exitCode = 0;

		const cliParams = {
			headless: true,
			'reinit-browser': false,
			close: true
		};

		Object.keys(cliParams).forEach((key) => {
			cliParams[key] = args[`--${key}`] ? JSON.parse(args[`--${key}`]) : cliParams[key];
		});

		// eslint-disable-next-line require-atomic-updates
		args['--port'] = args['--port'] || await portfinder.getPortPromise({
			port: args['--start-port'] || START_PORT
		});

		// eslint-disable-next-line require-atomic-updates
		args['--page'] = args['--page'] || build.demoPage;

		const
			fileServer = new nodeStatic.Server(src.output(args['--client-name']));

		const server = http.createServer((req, res) => {
			req.addListener('end', () => {
				fileServer.serve(req, res);
			}).resume();
		}).listen(args['--port']);

		const
			componentDir = resolve.blockSync(args['--name']),
			tmpDir = path.join(src.cwd(), 'tmp', path.relative(src.src(), componentDir));

		fs.mkdirpSync(tmpDir);

		const
			entryPoint = args['--test-entry'],
			testPath = entryPoint ? resolve.blockSync(entryPoint) : path.join(componentDir, 'test'),
			test = require(testPath);

		const browserParams = {
			chromium: {},
			firefox: {},
			webkit: {}
		};

		const
			browsersPromises = [];

		for (const browserType of browsers) {
			browsersPromises.push(createBrowser(browserType));
		}

		await Promise.all(browsersPromises);

		for (const browserType of browsers) {
			await runTest(browserType);
		}

		await server.close();

		function getTestEnv(browserType) {
			const
				Jasmine = require('jasmine'),
				jasmine = new Jasmine();

			jasmine.configureDefaultReporter({});
			Object.assign(globalThis, jasmine.env);

			globalThis.jasmine.DEFAULT_TIMEOUT_INTERVAL = (10).seconds();

			console.log('\n-------------');
			console.log('Starting to test');

			pzlr.config.dependencies.forEach((dep) => {
				console.log(`${dep} version: ${require(`${dep}/package.json`).version}`);
			});

			console.log(`env component: ${args['--name']}`);
			console.log(`test entry: ${args['--test-entry']}`);
			console.log(`runner: ${args['--runner']}`);
			console.log(`browser: ${browserType}`);
			console.log('-------------\n');

			return jasmine.env;
		}

		async function createBrowser(browserType) {
			const browserInstanceParams = {
				headless: cliParams.headless
			};

			const browserInstanceOptions = {
				reInit: cliParams['reinit-browser']
			};

			const
				browser = await getBrowserInstance(browserType, browserInstanceParams, browserInstanceOptions),
				device = args['--device']?.replace(/_/g, ' ');

			if (device != null && devices[device] == null) {
				throw Error(`The specified devise "${device}" is not supported`);
			}

			const contextOpts = {
				...device ? devices[device] : null
			};

			const
				context = await browser.newContext(contextOpts),
				page = await context.newPage();

			const
				testURL = `localhost:${args['--port']}/${args['--page']}.html`;

			browserParams[browserType] = {
				testURL,
				browser,
				browserType,
				page,
				context,
				componentDir,
				tmpDir
			};
		}

		async function runTest(browserType) {
			const
				params = browserParams[browserType];

			const {
				testURL,
				page
			} = params;

			await page.goto(testURL);
			const testEnv = getTestEnv(browserType);

			const close = () => {
				if (!cliParams['reinit-browser'] && cliParams.close) {
					params.browser.close();
				}

				process.exitCode = exitCode;
			};

			if (await test(page, params) === false) {
				close();
				return;
			}

			testEnv.addReporter({
				specDone: (res) => {
					if (exitCode === 1) {
						return;
					}

					exitCode = res.status === 'failed' ? 1 : 0;
				}
			});

			await new Promise((resolve) => {
				testEnv.afterAll(() => {
					console.log('\n\n\n');
					resolve();
				}, 10e3);

				testEnv.execute();
			}).then(close, close);
		}
	});

	/**
	 * Builds and runs tests for the specified component
	 *
	 * @see test:component:build
	 * @see test:component:run
	 *
	 * @example
	 * ```bash
	 * npx gulp test:component --name b-button --browsers ff,chrome
	 * ```
	 */
	gulp.task('test:component', gulp.series([
		'test:component:build',
		'test:component:run'
	]));

	/**
	 * Builds and runs tests for all components.
	 * Arguments:
	 *
	 * * [--processes] | [-p] - number of available CPUs to build the application and run tests
	 * * [--test-processes] | [-tp] - number of available CPUs to run tests
	 * * [--build-processes] | [-tb] - number of available CPUs to build the application
	 * * [--only-run] - allows run all test cases without the building stage
	 *
	 * @see test:component:run
	 * @see test:component:build
	 *
	 * @example
	 * ```bash
	 * npx gulp test:components -p 16
	 * ```
	 */
	gulp.task('test:components', async (cb) => {
		console.log(`CPUS available: ${cpus}`);

		const
			playwright = require('playwright');

		const
			{cwd} = resolve,
			cases = require(path.join(cwd, 'tests/cases.js'));

		const processesArgs = arg({
			'--processes': Number,
			'--test-processes': Number,
			'--build-processes': Number,

			'-p': '--processes',
			'-tp': '--test-processes',
			'-bp': '--build-processes'
		}, {permissive: true});

		const cliArgs = arg({
			'--reinit-browser': String,
			'--only-run': Boolean,
			'--browser-args': String
		}, {permissive: true});

		const cliParams = {
			reInitBrowser: cliArgs['--reinit-browser'] ? JSON.parse(cliArgs['--reinit-browser']) : false,
			browserArgs: cliArgs['--browser-args'] ? cliArgs['--browser-args'].split(',') : []
		};

		const
			buildProcess = processesArgs['--build-processes'] || processesArgs['--processes'] || cpus,
			testProcess = processesArgs['--test-processes'] || processesArgs['--processes'] || cpus;

		const
			waitForQuotas = (map, maxQueue) => wait(() => map.size < maxQueue),
			waitForEmpty = (map) => wait(() => map.size === 0);

		const exec = (execString, successCb, failCb) => new Promise((res, rej) => {
			$.run(execString, {verbosity: 3})
				.exec('', () => {
					successCb && successCb();
					res();
				})

				.on('error', (err) => {
					failCb && failCb(execString, err);
					rej();
				});
		});

		// Build components

		const
			buildCache = {},
			buildMap = new Map();

		if (!cliArgs['--only-run']) {
			for (let i = 0; i < cases.length; i++) {
				let
					c = cases[i];

				if (!c.includes('--name')) {
					c = `${c} --runtime-render true`;
				}

				const args = arg({
					'--name': String
				}, {argv: c.split(' '), permissive: true});

				args['--client-name'] = getTestClientName(args['--name'], build.suit);

				if (buildCache[args['--client-name']]) {
					continue;
				}

				const argsString = [
					['--suit', build.suit],
					['--name', args['--name']],
					['--client-name', args['--client-name']]
				].flat().join(' ');

				const extraArgs = args._.join(' ');

				await waitForQuotas(buildMap, buildProcess);

				buildMap.set(
					argsString,
					exec(`npx gulp test:component:build ${argsString} ${extraArgs}`, () => buildMap.delete(argsString))
				);

				buildCache[args['--client-name']] = true;
			}
		}

		await waitForEmpty(buildMap);

		// Launch browser server

		let endpointArg = '';

		const wsEndpoints = {
			chromium: '',
			firefox: '',
			webkit: ''
		};

		const
			servers = {},
			browsers = getSelectedBrowsers();

		if (!cliParams.reInitBrowser) {
			for (const browserType of browsers) {
				const
					browser = await playwright[browserType].launchServer({args: getBrowserArgs()}),
					wsEndpoint = browser.wsEndpoint();

				servers[browserType] = browser;
				await playwright[browserType].connect({wsEndpoint});
				wsEndpoints[browserType] = wsEndpoint;
			}

			endpointArg = Object.entries(wsEndpoints).map(([key, value]) => `--${key}WsEndpoint ${value}`).join(' ');
		}

		// Run tests

		const
			totalCases = [],
			failedCases = [],
			testMap = new Map();

		for (let i = 0; i < cases.length; i++) {
			let
				c = cases[i];

			if (!c.includes('--name')) {
				c = `${c} --runtime-render true`;
			}

			// Set the beginning of the searching range for a free port
			c = `${c} --start-port ${START_PORT + i * 100}`;

			const args = arg({
				'--name': String
			}, {argv: c.split(' '), permissive: true});

			args['--client-name'] = getTestClientName(args['--name'], build.suit);

			const
				browserArgs = cliParams.browserArgs.join(','),
				argsString = `${c} --client-name ${args['--client-name']} --browsers ${browsers.join(',')} --browser-args ${browserArgs}`,
				extraArgs = args._.join(' ');

			totalCases.push(argsString);
			await waitForQuotas(testMap, testProcess);

			const
				onTestEnd = (argsString) => testMap.delete(argsString);

			testMap.set(
				argsString,
				exec(`npx gulp test:component:run ${argsString} ${endpointArg} ${extraArgs}`,
					() => onTestEnd(argsString),
					() => {
						onTestEnd(argsString);
						failedCases.push(argsString);
					})
			);
		}

		await waitForEmpty(testMap);

		console.log('\n-------------');
		console.log(`\n✔ Tests passed: ${totalCases.filter((v) => !failedCases.includes(v)).length}`);
		console.log(`\n❌ Tests failed: ${failedCases.length}`);

		if (failedCases.length) {
			console.log('\n❗ Failed tests:');
			console.log(`\n${failedCases.join('\n')}`);
		}

		console.log('\n-------------\n');

		if (!cliParams.reInitBrowser) {
			Object.keys(servers).forEach((key) => servers[key].close());
		}

		if (failedCases.length) {
			throw new Error('Tests wasn\'t passed');
		}

		cb();
	});
};

{'fraction_non_alphanumeric': 0.19105072831119674, 'fraction_numerical': 0.002771300145324276, 'mean_word_length': 3.088145896656535, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 55, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2925449', 'n_tokens_mistral': 11531, 'n_tokens_neox': 9673, 'n_words': 2214}