| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const path = require('path'); | 
					
						
							|  |  |  | const execa = require('execa'); | 
					
						
							|  |  |  | const fs = require('node:fs/promises'); | 
					
						
							|  |  |  | const yargs = require('yargs'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const chalk = require('chalk'); | 
					
						
							|  |  |  | const { cleanTestApp, generateTestApp } = require('../helpers/test-app'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const cwd = path.resolve(__dirname, '../..'); | 
					
						
							|  |  |  | const testAppDirectory = path.join(cwd, 'test-apps', 'cli'); | 
					
						
							| 
									
										
										
										
											2024-04-03 11:56:00 +02:00
										 |  |  | const testRoot = path.join(cwd, 'tests', 'cli'); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  | const testsDir = path.join(testRoot, 'tests'); | 
					
						
							|  |  |  | const templateDir = path.join(testRoot, 'app-template'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | yargs | 
					
						
							|  |  |  |   .parserConfiguration({ | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * This lets us pass any other arguments to the test runner | 
					
						
							|  |  |  |      * e.g. the name of a specific test or the project we want to run | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     'unknown-options-as-args': true, | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  |   .command({ | 
					
						
							|  |  |  |     command: '*', | 
					
						
							|  |  |  |     description: 'run the CLI test suite', | 
					
						
							|  |  |  |     async builder(yarg) { | 
					
						
							|  |  |  |       // each directory in testDir is a domain
 | 
					
						
							|  |  |  |       const domains = await fs.readdir(testsDir); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       yarg.option('concurrency', { | 
					
						
							|  |  |  |         alias: 'c', | 
					
						
							|  |  |  |         type: 'number', | 
					
						
							|  |  |  |         default: domains.length, | 
					
						
							|  |  |  |         describe: 'Number of concurrent test domains to run', | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       yarg.option('domains', { | 
					
						
							|  |  |  |         alias: 'd', | 
					
						
							|  |  |  |         describe: 'Run a specific test suite domain', | 
					
						
							|  |  |  |         type: 'array', | 
					
						
							|  |  |  |         choices: domains, | 
					
						
							|  |  |  |         default: domains, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       yarg.option('setup', { | 
					
						
							|  |  |  |         alias: 'f', | 
					
						
							|  |  |  |         describe: 'Force the setup process of the test apps', | 
					
						
							|  |  |  |         type: 'boolean', | 
					
						
							|  |  |  |         default: false, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     async handler(argv) { | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         const { concurrency, domains, setup } = argv; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * Publishing all packages to the yalc store | 
					
						
							|  |  |  |          */ | 
					
						
							|  |  |  |         console.log('Running yalc...'); | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |         await execa('node', [path.join(__dirname, '../..', 'scripts', 'yalc-publish.js')]); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |         console.log('Complete'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const loadDomainConfigs = async (domain) => { | 
					
						
							|  |  |  |           try { | 
					
						
							|  |  |  |             const configPath = path.join(testsDir, domain, 'config.js'); | 
					
						
							|  |  |  |             await fs.access(configPath); | 
					
						
							|  |  |  |             // Import config.js and call it as a function
 | 
					
						
							|  |  |  |             const config = require(configPath); | 
					
						
							|  |  |  |             if (typeof config === 'function') { | 
					
						
							|  |  |  |               return await config(argv); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return config; | 
					
						
							|  |  |  |           } catch (e) { | 
					
						
							|  |  |  |             // use default config
 | 
					
						
							|  |  |  |             return { | 
					
						
							|  |  |  |               testApps: 1, | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Load the domain configs into an object with keys of the name of the test domain
 | 
					
						
							|  |  |  |         const domainConfigs = {}; | 
					
						
							|  |  |  |         await Promise.all( | 
					
						
							|  |  |  |           domains.map(async (domain) => { | 
					
						
							|  |  |  |             domainConfigs[domain] = await loadDomainConfigs(domain); | 
					
						
							|  |  |  |           }) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Determine the number of simultaneous test apps we need by taking the concurrency number of highest testApps requested from config
 | 
					
						
							|  |  |  |         const testAppsRequired = Object.entries(domainConfigs) | 
					
						
							|  |  |  |           .map(([, value]) => value.testApps) // Extract testApps values from config
 | 
					
						
							|  |  |  |           .sort((a, b) => b - a) // Sort in descending order
 | 
					
						
							|  |  |  |           .slice(0, concurrency) // Take the top X values
 | 
					
						
							|  |  |  |           .reduce((acc, value) => acc + value, 0); // Sum up the values
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (testAppsRequired === 0) { | 
					
						
							|  |  |  |           throw new Error('No test apps to spawn'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const testAppPaths = Array.from({ length: testAppsRequired }, (_, i) => | 
					
						
							|  |  |  |           path.join(testAppDirectory, `test-app-${i}`) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         let currentTestApps = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |           currentTestApps = await fs | 
					
						
							|  |  |  |             .readdir(testAppDirectory) | 
					
						
							|  |  |  |             .then((paths) => paths.map((appPath) => path.join(testAppDirectory, appPath))); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |         } catch (err) { | 
					
						
							|  |  |  |           // no test apps exist, okay to fail silently
 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * If we don't have enough test apps, we make enough. | 
					
						
							|  |  |  |          * You can also force this setup if desired, e.g. you | 
					
						
							|  |  |  |          * update the app-template. | 
					
						
							|  |  |  |          */ | 
					
						
							|  |  |  |         if (setup || currentTestApps.length < testAppsRequired) { | 
					
						
							|  |  |  |           /** | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |            * this will effectively clean the entire directory beforehand | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |            * as opposed to cleaning the ones we aim to spawn. | 
					
						
							|  |  |  |            */ | 
					
						
							|  |  |  |           await Promise.all( | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |             currentTestApps.map(async (appPath) => { | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |               console.log(`Cleaning test app at path: ${chalk.bold(appPath)}`); | 
					
						
							|  |  |  |               await cleanTestApp(appPath); | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |           currentTestApps = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |           /** | 
					
						
							|  |  |  |            * Generate the test apps and modify the configuration as needed | 
					
						
							|  |  |  |            */ | 
					
						
							|  |  |  |           await Promise.all( | 
					
						
							|  |  |  |             testAppPaths.map(async (appPath) => { | 
					
						
							|  |  |  |               console.log(`Generating test apps at path: ${chalk.bold(appPath)}`); | 
					
						
							|  |  |  |               await generateTestApp({ | 
					
						
							|  |  |  |                 appPath, | 
					
						
							|  |  |  |                 database: { | 
					
						
							|  |  |  |                   client: 'sqlite', | 
					
						
							|  |  |  |                   connection: { | 
					
						
							|  |  |  |                     filename: './.tmp/data.db', | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                   useNullAsDefault: true, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 template: templateDir, | 
					
						
							|  |  |  |                 link: true, | 
					
						
							|  |  |  |               }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               /** | 
					
						
							|  |  |  |                * Because we're running multiple test apps at the same time | 
					
						
							|  |  |  |                * and the env file is generated by the generator with no way | 
					
						
							|  |  |  |                * to override it, we manually remove the PORT key/value so when | 
					
						
							|  |  |  |                * we set it further down for each playwright instance it works. | 
					
						
							|  |  |  |                */ | 
					
						
							|  |  |  |               const pathToEnv = path.join(appPath, '.env'); | 
					
						
							|  |  |  |               const envFile = (await fs.readFile(pathToEnv)).toString(); | 
					
						
							|  |  |  |               const envWithoutPort = envFile.replace('PORT=1337', ''); | 
					
						
							|  |  |  |               await fs.writeFile(pathToEnv, envWithoutPort); | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |               currentTestApps.push(appPath); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |             }) | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           console.log( | 
					
						
							|  |  |  |             `${chalk.green('Successfully')} setup test apps for the following domains: ${chalk.bold( | 
					
						
							|  |  |  |               domains.join(', ') | 
					
						
							|  |  |  |             )}`
 | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           console.log( | 
					
						
							|  |  |  |             `Skipping setting up test apps, use ${chalk.bold('--setup')} to force the setup process` | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * Run the tests in parallel based on concurrency value | 
					
						
							|  |  |  |          * */ | 
					
						
							|  |  |  |         const availableTestApps = [...currentTestApps]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const batches = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for (let i = 0; i < domains.length; i += concurrency) { | 
					
						
							|  |  |  |           batches.push(domains.slice(i, i + concurrency)); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // eslint-disable-next-line no-plusplus
 | 
					
						
							|  |  |  |         for (let i = 0; i < batches.length; i++) { | 
					
						
							|  |  |  |           const batch = batches[i]; | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |           let failingTests = 0; | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |           await Promise.all( | 
					
						
							|  |  |  |             batch.map(async (domain) => { | 
					
						
							|  |  |  |               const config = domainConfigs[domain]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               if (availableTestApps.length < config.testApps) { | 
					
						
							|  |  |  |                 console.error('Not enough test apps available; aborting'); | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |                 process.exit(1); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               // claim testApps for this domain to use
 | 
					
						
							|  |  |  |               const testApps = availableTestApps.splice(-1 * config.testApps); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               /** | 
					
						
							|  |  |  |                * We do not start up the apps; the test runner is responsible for that if it's necessary, | 
					
						
							|  |  |  |                * but most CLI commands don't need a started instance of strapi | 
					
						
							|  |  |  |                * Instead, we just pass in the path of the test apps assigned for this test runner via env | 
					
						
							|  |  |  |                *  */ | 
					
						
							|  |  |  |               try { | 
					
						
							|  |  |  |                 const env = { | 
					
						
							|  |  |  |                   TEST_APPS: testApps.join(','), | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |                   JWT_SECRET: 'test-jwt-secret', | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |                 }; | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |                 const domainDir = path.join(testsDir, domain); | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |                 console.log('Running jest for domain', domain, 'in', domainDir); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |                 // run the command 'jest --rootDir <domainDir>'
 | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |                 await execa( | 
					
						
							|  |  |  |                   'jest', | 
					
						
							|  |  |  |                   [ | 
					
						
							|  |  |  |                     '--config', | 
					
						
							| 
									
										
										
										
											2024-04-03 11:56:00 +02:00
										 |  |  |                     '../../../../jest.config.cli.js', | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |                     '--rootDir', | 
					
						
							|  |  |  |                     domainDir, | 
					
						
							|  |  |  |                     '--color', | 
					
						
							|  |  |  |                     '--verbose', | 
					
						
							|  |  |  |                     '--runInBand', // tests must not run concurrently
 | 
					
						
							|  |  |  |                   ], | 
					
						
							|  |  |  |                   { | 
					
						
							|  |  |  |                     stdio: 'inherit', | 
					
						
							|  |  |  |                     cwd: domainDir, // run from the domain directory
 | 
					
						
							|  |  |  |                     env, // pass it our custom env values
 | 
					
						
							|  |  |  |                     timeout: 2 * 60 * 1000, // 2 minutes
 | 
					
						
							|  |  |  |                   } | 
					
						
							|  |  |  |                 ); | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |               } catch (err) { | 
					
						
							|  |  |  |                 // If any tests fail
 | 
					
						
							|  |  |  |                 console.error('Test suite failed for', domain); | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |                 failingTests += 1; | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               // make them available again for the next batch
 | 
					
						
							|  |  |  |               availableTestApps.push(...testApps); | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2024-02-09 10:44:33 +01:00
										 |  |  |           if (failingTests > 0) { | 
					
						
							|  |  |  |             throw new Error(`${failingTests} tests failed`); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2024-01-08 11:23:26 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         console.error(chalk.red('Error running CLI tests:')); | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * This is a ExecaError, if we were in TS we could do `instanceof` | 
					
						
							|  |  |  |          */ | 
					
						
							|  |  |  |         if (err.shortMessage) { | 
					
						
							|  |  |  |           console.error(err.shortMessage); | 
					
						
							|  |  |  |           process.exit(1); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         console.error(err); | 
					
						
							|  |  |  |         process.exit(1); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  |   .command({ | 
					
						
							|  |  |  |     command: 'clean', | 
					
						
							|  |  |  |     description: 'clean the test app directory of all test apps', | 
					
						
							|  |  |  |     async handler() { | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         const currentTestApps = await fs.readdir(testAppDirectory); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (currentTestApps.length === 0) { | 
					
						
							|  |  |  |           console.log('No test apps to clean'); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await Promise.all( | 
					
						
							|  |  |  |           currentTestApps.map(async (testAppName) => { | 
					
						
							|  |  |  |             const appPath = path.join(testAppDirectory, testAppName); | 
					
						
							|  |  |  |             console.log(`Cleaning test app at path: ${chalk.bold(appPath)}`); | 
					
						
							|  |  |  |             await cleanTestApp(appPath); | 
					
						
							|  |  |  |           }) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         console.error(chalk.red('Error cleaning test apps:')); | 
					
						
							|  |  |  |         console.error(err); | 
					
						
							|  |  |  |         process.exit(1); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  |   .help() | 
					
						
							|  |  |  |   .parse(); |