mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 03:43:34 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			309 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			309 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'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 dotenv = require('dotenv');
 | 
						|
const { cleanTestApp, generateTestApp } = require('../helpers/test-app');
 | 
						|
const { createConfig } = require('../../playwright.base.config');
 | 
						|
 | 
						|
const cwd = path.resolve(__dirname, '../..');
 | 
						|
const testAppDirectory = path.join(cwd, 'test-apps', 'e2e');
 | 
						|
const testRoot = path.join(cwd, 'tests', 'e2e');
 | 
						|
const testDomainRoot = path.join(testRoot, 'tests');
 | 
						|
const templateDir = path.join(testRoot, 'app-template');
 | 
						|
 | 
						|
const pathExists = async (path) => {
 | 
						|
  try {
 | 
						|
    await fs.access(path);
 | 
						|
    return true;
 | 
						|
  } catch (err) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Updates the env file for a generated test app
 | 
						|
 * - Removes the PORT key/value from generated app .env
 | 
						|
 * - Uses e2e/app-template/config/features.js to enable future features in the generated app
 | 
						|
 */
 | 
						|
const setupTestEnvironment = async (generatedAppPath) => {
 | 
						|
  /**
 | 
						|
   * 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(generatedAppPath, '.env');
 | 
						|
  const envFile = (await fs.readFile(pathToEnv)).toString();
 | 
						|
  const envWithoutPort = envFile.replace('PORT=1337', '');
 | 
						|
  await fs.writeFile(pathToEnv, envWithoutPort);
 | 
						|
 | 
						|
  /*
 | 
						|
   * Enable future features in the generated app manually since a template
 | 
						|
   * does not allow the config folder.
 | 
						|
   */
 | 
						|
  const testRootFeaturesConfigPath = path.join(templateDir, 'config', 'features.js');
 | 
						|
  const hasFeaturesConfig = await pathExists(testRootFeaturesConfigPath);
 | 
						|
 | 
						|
  if (!hasFeaturesConfig) return;
 | 
						|
 | 
						|
  const configFeatures = await fs.readFile(testRootFeaturesConfigPath);
 | 
						|
  const appFeaturesConfigPath = path.join(generatedAppPath, 'config', 'features.js');
 | 
						|
  await fs.writeFile(appFeaturesConfigPath, configFeatures);
 | 
						|
};
 | 
						|
 | 
						|
yargs
 | 
						|
  .parserConfiguration({
 | 
						|
    /**
 | 
						|
     * When unknown options is false, using -- to separate playwright args from test:e2e args works
 | 
						|
     * When it is true, the script gets confused about additional arguments, with or without using -- to separate commands
 | 
						|
     */
 | 
						|
    'unknown-options-as-args': false,
 | 
						|
  })
 | 
						|
  .command({
 | 
						|
    command: '*',
 | 
						|
    description: 'run the E2E test suite',
 | 
						|
    async builder(yarg) {
 | 
						|
      const domains = await fs.readdir(testDomainRoot);
 | 
						|
 | 
						|
      yarg.option('concurrency', {
 | 
						|
        alias: 'c',
 | 
						|
        type: 'number',
 | 
						|
        default: domains.length,
 | 
						|
        describe:
 | 
						|
          'Number of concurrent test apps to run, a test app runs an entire test suite domain',
 | 
						|
      });
 | 
						|
 | 
						|
      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 {
 | 
						|
        if (await pathExists(path.join(testRoot, '.env'))) {
 | 
						|
          // Run tests with the env variables specified in the e2e/app-template/.env
 | 
						|
          dotenv.config({ path: path.join(testRoot, '.env') });
 | 
						|
        }
 | 
						|
 | 
						|
        const { concurrency, domains, setup } = argv;
 | 
						|
 | 
						|
        /**
 | 
						|
         * Publishing all packages to the yalc store
 | 
						|
         */
 | 
						|
        await execa('node', [path.join(__dirname, '../..', 'scripts', 'yalc-publish.js')], {
 | 
						|
          stdio: 'inherit',
 | 
						|
        });
 | 
						|
 | 
						|
        /**
 | 
						|
         * We don't need to spawn more apps than we have domains,
 | 
						|
         * but equally if someone sets the concurrency to 1
 | 
						|
         * then we should only spawn one and run every domain on there.
 | 
						|
         */
 | 
						|
        const testAppsToSpawn = Math.min(domains.length, concurrency);
 | 
						|
 | 
						|
        if (testAppsToSpawn === 0) {
 | 
						|
          throw new Error('No test apps to spawn');
 | 
						|
        }
 | 
						|
 | 
						|
        const testAppPaths = Array.from({ length: testAppsToSpawn }, (_, i) =>
 | 
						|
          path.join(testAppDirectory, `test-app-${i}`)
 | 
						|
        );
 | 
						|
 | 
						|
        let currentTestApps = [];
 | 
						|
 | 
						|
        try {
 | 
						|
          currentTestApps = await fs.readdir(testAppDirectory);
 | 
						|
        } 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 < testAppsToSpawn) {
 | 
						|
          /**
 | 
						|
           * this will effectively clean the entire directory before hand
 | 
						|
           * as opposed to cleaning the ones we aim to spawn.
 | 
						|
           */
 | 
						|
          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);
 | 
						|
            })
 | 
						|
          );
 | 
						|
 | 
						|
          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,
 | 
						|
              });
 | 
						|
 | 
						|
              await setupTestEnvironment(appPath);
 | 
						|
            })
 | 
						|
          );
 | 
						|
 | 
						|
          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`
 | 
						|
          );
 | 
						|
        }
 | 
						|
 | 
						|
        /**
 | 
						|
         * You can't change the webserver configuration of playwright directly so they'd
 | 
						|
         * all be looking at the same test app which we don't want, instead we'll generate
 | 
						|
         * a playwright config based off the base one
 | 
						|
         */
 | 
						|
        const chunkedDomains = domains.reduce((acc, _, i) => {
 | 
						|
          if (i % testAppsToSpawn === 0) acc.push(domains.slice(i, i + testAppsToSpawn));
 | 
						|
          return acc;
 | 
						|
        }, []);
 | 
						|
 | 
						|
        // eslint-disable-next-line no-plusplus
 | 
						|
        for (let i = 0; i < chunkedDomains.length; i++) {
 | 
						|
          const domains = chunkedDomains[i];
 | 
						|
 | 
						|
          await Promise.all(
 | 
						|
            domains.map(async (domain, j) => {
 | 
						|
              const testAppPath = testAppPaths[j];
 | 
						|
              const port = 8000 + j;
 | 
						|
 | 
						|
              const pathToPlaywrightConfig = path.join(testAppPath, 'playwright.config.js');
 | 
						|
 | 
						|
              console.log(
 | 
						|
                `Creating playwright config for domain: ${chalk.blue(
 | 
						|
                  domain
 | 
						|
                )}, at path: ${chalk.yellow(testAppPath)}`
 | 
						|
              );
 | 
						|
 | 
						|
              const config = createConfig({
 | 
						|
                testDir: path.join(testDomainRoot, domain),
 | 
						|
                port,
 | 
						|
                appDir: testAppPath,
 | 
						|
              });
 | 
						|
 | 
						|
              const configFileTemplate = `
 | 
						|
const config = ${JSON.stringify(config)}
 | 
						|
 | 
						|
module.exports = config
 | 
						|
              `;
 | 
						|
 | 
						|
              await fs.writeFile(pathToPlaywrightConfig, configFileTemplate);
 | 
						|
 | 
						|
              // Store the filesystem state with git so it can be reset between tests
 | 
						|
              // TODO: if we have a large test test suite, it might be worth it to run a `strapi start` and then shutdown here to generate documentation and types only once and save unneccessary server restarts from those files being cleared every time
 | 
						|
              console.log('Initializing git');
 | 
						|
 | 
						|
              const gitUser = ['-c', 'user.name=Strapi CLI', '-c', 'user.email=test@strapi.io'];
 | 
						|
 | 
						|
              await execa('git', [...gitUser, 'init'], {
 | 
						|
                stdio: 'inherit',
 | 
						|
                cwd: testAppPath,
 | 
						|
              });
 | 
						|
 | 
						|
              // we need to use -A to track even hidden files like .env; remember we're only using git as a file state manager
 | 
						|
              await execa('git', [...gitUser, 'add', '-A', '.'], {
 | 
						|
                stdio: 'inherit',
 | 
						|
                cwd: testAppPath,
 | 
						|
              });
 | 
						|
 | 
						|
              await execa('git', [...gitUser, 'commit', '-m', 'initial commit'], {
 | 
						|
                stdio: 'inherit',
 | 
						|
                cwd: testAppPath,
 | 
						|
              });
 | 
						|
 | 
						|
              console.log(`Running ${chalk.blue(domain)} e2e tests`);
 | 
						|
 | 
						|
              await execa(
 | 
						|
                'yarn',
 | 
						|
                ['playwright', 'test', '--config', pathToPlaywrightConfig, ...argv._],
 | 
						|
                {
 | 
						|
                  stdio: 'inherit',
 | 
						|
                  cwd,
 | 
						|
                  env: {
 | 
						|
                    PORT: port,
 | 
						|
                    HOST: '127.0.0.1',
 | 
						|
                    TEST_APP_PATH: testAppPath,
 | 
						|
                    STRAPI_DISABLE_EE: !process.env.STRAPI_LICENSE,
 | 
						|
                  },
 | 
						|
                }
 | 
						|
              );
 | 
						|
            })
 | 
						|
          );
 | 
						|
        }
 | 
						|
      } catch (err) {
 | 
						|
        console.error(chalk.red('Error running e2e 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();
 |