2023-07-13 11:37:46 +01:00
'use strict' ;
const path = require ( 'path' ) ;
const execa = require ( 'execa' ) ;
const fs = require ( 'node:fs/promises' ) ;
const yargs = require ( 'yargs' ) ;
2023-12-14 16:15:21 +01:00
const chalk = require ( 'chalk' ) ;
2024-03-05 09:09:44 +01:00
const dotenv = require ( 'dotenv' ) ;
2023-07-13 11:37:46 +01:00
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' ) ;
2024-04-02 11:19:43 +02:00
const testRoot = path . join ( cwd , 'tests' , 'e2e' ) ;
const testDomainRoot = path . join ( testRoot , 'tests' ) ;
2024-01-08 11:23:26 +01:00
const templateDir = path . join ( testRoot , 'app-template' ) ;
2023-07-13 11:37:46 +01:00
2024-03-05 09:09:44 +01:00
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 ) ;
} ;
2023-07-13 11:37:46 +01:00
yargs
. parserConfiguration ( {
/ * *
2024-04-04 18:55:24 +02:00
* 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
2023-07-13 11:37:46 +01:00
* /
2024-04-03 11:56:00 +02:00
'unknown-options-as-args' : false ,
2023-07-13 11:37:46 +01:00
} )
. command ( {
command : '*' ,
description : 'run the E2E test suite' ,
2023-12-14 16:15:21 +01:00
async builder ( yarg ) {
2024-04-02 11:19:43 +02:00
const domains = await fs . readdir ( testDomainRoot ) ;
2023-07-13 11:37:46 +01:00
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 ,
} ) ;
} ,
2023-12-14 16:15:21 +01:00
async handler ( argv ) {
2023-07-13 11:37:46 +01:00
try {
2024-03-05 09:09:44 +01:00
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' ) } ) ;
}
2023-07-13 11:37:46 +01:00
const { concurrency , domains , setup } = argv ;
2023-12-01 11:30:16 +01:00
/ * *
2024-01-08 11:23:26 +01:00
* Publishing all packages to the yalc store
2023-12-01 11:30:16 +01:00
* /
await execa ( 'node' , [ path . join ( _ _dirname , '../..' , 'scripts' , 'yalc-publish.js' ) ] , {
stdio : 'inherit' ,
} ) ;
2023-07-13 11:37:46 +01:00
/ * *
* 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 : {
2023-07-21 13:20:41 +01:00
filename : './.tmp/data.db' ,
2023-07-13 11:37:46 +01:00
} ,
useNullAsDefault : true ,
} ,
2024-01-08 11:23:26 +01:00
template : templateDir ,
2023-07-27 15:53:08 +02:00
link : true ,
2023-07-13 11:37:46 +01:00
} ) ;
2024-03-05 09:09:44 +01:00
await setupTestEnvironment ( appPath ) ;
2023-07-13 11:37:46 +01:00
} )
) ;
console . log (
2023-07-21 13:20:41 +01:00
` ${ chalk . green ( 'Successfully' ) } setup test apps for the following domains: ${ chalk . bold (
2023-07-13 11:37:46 +01:00
domains . join ( ', ' )
) } `
) ;
} else {
console . log (
2023-07-19 16:34:58 +01:00
` Skipping setting up test apps, use ${ chalk . bold ( '--setup' ) } to force the setup process `
2023-07-13 11:37:46 +01:00
) ;
}
/ * *
* 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 ;
} , [ ] ) ;
2024-01-08 11:23:26 +01:00
// eslint-disable-next-line no-plusplus
2023-07-13 11:37:46 +01:00
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 ( {
2024-04-02 11:19:43 +02:00
testDir : path . join ( testDomainRoot , domain ) ,
2023-07-13 11:37:46 +01:00
port ,
appDir : testAppPath ,
} ) ;
const configFileTemplate = `
const config = $ { JSON . stringify ( config ) }
module . exports = config
` ;
await fs . writeFile ( pathToPlaywrightConfig , configFileTemplate ) ;
2024-04-04 18:55:24 +02:00
// 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 ,
} ) ;
2023-07-13 11:37:46 +01:00
console . log ( ` Running ${ chalk . blue ( domain ) } e2e tests ` ) ;
await execa (
'yarn' ,
[ 'playwright' , 'test' , '--config' , pathToPlaywrightConfig , ... argv . _ ] ,
{
stdio : 'inherit' ,
cwd ,
env : {
PORT : port ,
2023-07-19 16:34:58 +01:00
HOST : '127.0.0.1' ,
2024-04-04 18:55:24 +02:00
TEST _APP _PATH : testAppPath ,
2024-02-06 12:26:06 +01:00
STRAPI _DISABLE _EE : ! process . env . STRAPI _LICENSE ,
2023-07-13 11:37:46 +01:00
} ,
}
) ;
} )
) ;
}
} 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' ,
2023-12-14 16:15:21 +01:00
async handler ( ) {
2023-07-13 11:37:46 +01:00
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 ( ) ;