From fa6c3fc4ea30193965ee260e58fd6d1e8780443a Mon Sep 17 00:00:00 2001
From: Benjamin Franzke <ben@bnf.dev>
Date: Sat, 17 Feb 2024 10:33:23 +0100
Subject: [PATCH] [TASK] Add composer-mode to our acceptance test matrix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

All applicable acceptance tests are now also executed
in composer mode. Tests that check classic-mode specific
functions are excluded and are therefore now tagged
as classic-mode test.

The composer mode instance is generated with our
CLI setup tools, allowing to mimic the realworld
case where a instance is creating via our setup tools
instead of from fixtures (which we still use for classic
mode tests, but try to reducde/avoid for composer mode tests).

The existing existing classic-mode test acceptance test
execution will keep running as-is.

Resolves: #103297
Releases: main, 12.4, 11.5
Change-Id: I64973f110931b51ed2ef7ef8f8cc3411834fcf37
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83027
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Benjamin Franzke <ben@bnf.dev>
Tested-by: Benjamin Franzke <ben@bnf.dev>
---
 Build/Scripts/runTests.sh                     |  82 ++++++++-
 Build/Scripts/setupAcceptanceComposer.sh      |  50 ++++++
 Build/composer/composer.dist.json             |   6 +-
 Build/gitlab-ci.yml                           |   2 +
 .../acceptance-application-composer.yml       | 163 ++++++++++++++++++
 .../acceptance-application-composer.yml       |  17 ++
 .../Classes/Command/DatasetImportCommand.php  |  50 ++++++
 .../Configuration/Services.yaml               |   8 +
 .../packages/dataset_import/composer.json     |  20 +++
 .../Tests/Acceptance/Application.suite.yml    |   6 +-
 .../Application/BackendUser/ListUserCest.php  | 100 ++++++++---
 .../Extensionmanager/GetExtensionsCest.php    |   5 +
 .../InstalledExtensionsCest.php               |   3 +
 .../FormEngine/FalMetadataCest.php            |  10 +-
 .../Application/FormEngine/Inline1nCest.php   |  10 ++
 .../Frontend/ContentElementsCest.php          |  12 +-
 .../Frontend/FormFrameworkCest.php            |  12 +-
 .../Frontend/FrontendLoginCest.php            |  12 +-
 .../Frontend/IndexedSearchCest.php            |  12 +-
 .../Application/Impexp/ImportCest.php         |   9 +-
 .../Application/InstallTool/AbstractCest.php  |  20 ++-
 .../InstallTool/MaintenanceCest.php           |   2 +
 .../Application/InstallTool/SettingsCest.php  |  10 +-
 .../Application/InstallTool/UpgradeCest.php   |   2 +
 .../Redirect/RedirectModuleCest.php           |   2 +-
 .../Application/Scheduler/TasksCest.php       |   1 +
 .../Application/Template/TemplateCest.php     |   4 +
 .../ApplicationComposerEnvironment.php        |  49 ++++++
 .../Extension/ApplicationEnvironment.php      |   4 +
 29 files changed, 617 insertions(+), 66 deletions(-)
 create mode 100755 Build/Scripts/setupAcceptanceComposer.sh
 create mode 100644 Build/gitlab-ci/nightly/acceptance-application-composer.yml
 create mode 100644 Build/gitlab-ci/pre-merge/acceptance-application-composer.yml
 create mode 100644 Build/tests/packages/dataset_import/Classes/Command/DatasetImportCommand.php
 create mode 100644 Build/tests/packages/dataset_import/Configuration/Services.yaml
 create mode 100644 Build/tests/packages/dataset_import/composer.json
 create mode 100644 typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationComposerEnvironment.php

diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh
index af4723c7f565..9d4f7ed7a420 100755
--- a/Build/Scripts/runTests.sh
+++ b/Build/Scripts/runTests.sh
@@ -174,6 +174,7 @@ Options:
     -s <...>
         Specifies the test suite to run
             - acceptance: main application acceptance tests
+            - acceptanceComposer: main application acceptance tests
             - acceptanceInstall: installation acceptance tests, only with -d mariadb|postgres|sqlite
             - buildCss: execute scss to css builder
             - buildJavascript: execute typescript to javascript builder
@@ -236,7 +237,7 @@ Options:
                 - pdo_mysql
 
     -d <sqlite|mariadb|mysql|postgres>
-        Only with -s functional|functionalDeprecated|acceptance|acceptanceInstall
+        Only with -s functional|functionalDeprecated|acceptance|acceptanceComposer|acceptanceInstall
         Specifies on which DBMS tests are performed
             - sqlite: (default): use sqlite
             - mariadb: use mariadb
@@ -293,12 +294,12 @@ Options:
             Build/Scripts/runTests.sh -s unit -- --filter filterByValueRecursiveCorrectlyFiltersArray
 
     -g
-        Only with -s acceptance|acceptanceInstall
+        Only with -s acceptance|acceptanceComposer|acceptanceInstall
         Activate selenium grid as local port to watch browser clicking around. Can be surfed using
         http://localhost:7900/. A browser tab is opened automatically if xdg-open is installed.
 
     -x
-        Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom|acceptance|acceptanceInstall
+        Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom|acceptance|acceptanceComposer|acceptanceInstall
         Send information to host instance for test or system under test break points. This is especially
         useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port
         can be selected with -y
@@ -566,9 +567,9 @@ fi
 # Suite execution
 case ${TEST_SUITE} in
     acceptance)
-        CODECEPION_ENV="--env ci"
+        CODECEPION_ENV="--env ci,classic"
         if [ "${ACCEPTANCE_HEADLESS}" -eq 1 ]; then
-            CODECEPION_ENV="--env ci,headless"
+            CODECEPION_ENV="--env ci,classic,headless"
         fi
         if [ "${CHUNKS}" -gt 0 ]; then
             ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name ac-splitter-${SUFFIX} ${IMAGE_PHP} php -dxdebug.mode=off Build/Scripts/splitAcceptanceTests.php -v ${CHUNKS}
@@ -630,6 +631,75 @@ case ${TEST_SUITE} in
                 ;;
         esac
         ;;
+    acceptanceComposer)
+        rm -rf "${CORE_ROOT}/typo3temp/var/tests/acceptance-composer" "${CORE_ROOT}/typo3temp/var/tests/AcceptanceReports"
+
+        PREPAREPARAMS=""
+        TESTPARAMS=""
+        case ${DBMS} in
+            mariadb)
+                ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mariadb-ac-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=acp -e MYSQL_DATABASE=ac_test --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MARIADB} >/dev/null
+                waitFor mariadb-ac-${SUFFIX} 3306
+                PREPAREPARAMS="-e TYPO3_DB_DRIVER=mysqli -e TYPO3_DB_DBNAME=ac_test -e TYPO3_DB_USERNAME=root -e TYPO3_DB_PASSWORD=acp -e TYPO3_DB_HOST=mariadb-ac-${SUFFIX} -e TYPO3_DB_PORT=3306"
+                TESTPARAMS="-e typo3DatabaseName=ac_test -e typo3DatabaseUsername=root -e typo3DatabasePassword=funcp -e typo3DatabaseHost=mariadb-ac-${SUFFIX}"
+                ;;
+            mysql)
+                ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mysql-ac-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=acp -e MYSQL_DATABASE=ac_test --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MYSQL} >/dev/null
+                waitFor mysql-ac-${SUFFIX} 3306
+                PREPAREPARAMS="-e TYPO3_DB_DRIVER=mysqli -e TYPO3_DB_DBNAME=ac_test -e TYPO3_DB_USERNAME=root -e TYPO3_DB_PASSWORD=acp -e TYPO3_DB_HOST=mysql-ac-${SUFFIX} -e TYPO3_DB_PORT=3306"
+                TESTPARAMS="-e typo3DatabaseName=ac_test -e typo3DatabaseUsername=root -e typo3DatabasePassword=funcp -e typo3DatabaseHost=mysql-ac-${SUFFIX}"
+                ;;
+            postgres)
+                ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name postgres-ac-${SUFFIX} --network ${NETWORK} -d -e POSTGRES_DB=ac_test -e POSTGRES_PASSWORD=acp -e POSTGRES_USER=ac_test --tmpfs /var/lib/postgresql/data:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null
+                waitFor postgres-ac-${SUFFIX} 5432
+                PREPAREPARAMS="-e TYPO3_DB_DRIVER=postgres -e TYPO3_DB_DBNAME=ac_test -e TYPO3_DB_USERNAME=ac_test -e TYPO3_DB_PASSWORD=acp -e TYPO3_DB_HOST=postgres-ac-${SUFFIX} -e TYPO3_DB_PORT=5432"
+                TESTPARAMS="-e typo3DatabaseDriver=pdo_pgsql -e typo3DatabaseName=ac_test -e typo3DatabaseUsername=ac_test -e typo3DatabasePassword=acp -e typo3DatabaseHost=postgres-ac-${SUFFIX}"
+                ;;
+            sqlite)
+                PREPAREPARAMS="-e TYPO3_DB_DRIVER=sqlite"
+                TESTPARAMS="-e typo3DatabaseDriver=pdo_sqlite"
+                ;;
+        esac
+
+        ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name acceptance-prepare ${XDEBUG_MODE} -e COMPOSER_CACHE_DIR=${CORE_ROOT}/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${PREPAREPARAMS} ${IMAGE_PHP} "${CORE_ROOT}/Build/Scripts/setupAcceptanceComposer.sh" "typo3temp/var/tests/acceptance-composer"
+        SUITE_EXIT_CODE=$?
+        if [[ ${SUITE_EXIT_CODE} -eq 0 ]]; then
+            CODECEPION_ENV="--env ci,composer"
+            if [ "${ACCEPTANCE_HEADLESS}" -eq 1 ]; then
+                CODECEPION_ENV="--env ci,composer,headless"
+            fi
+            if [ "${CHUNKS}" -gt 0 ]; then
+                ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name ac-splitter-${SUFFIX} ${IMAGE_PHP} php -dxdebug.mode=off Build/Scripts/splitAcceptanceTests.php -v ${CHUNKS}
+                COMMAND=(bin/codecept run Application -d -g AcceptanceTests-Job-${THISCHUNK} -c typo3/sysext/core/Tests/codeception.yml ${EXTRA_TEST_OPTIONS} ${CODECEPION_ENV} "$@" --html reports.html)
+            else
+                COMMAND=(bin/codecept run Application -d -c typo3/sysext/core/Tests/codeception.yml ${EXTRA_TEST_OPTIONS} ${CODECEPION_ENV} "$@" --html reports.html)
+            fi
+            SELENIUM_GRID=""
+            if [ "${ACCEPTANCE_HEADLESS}" -eq 0 ]; then
+                SELENIUM_GRID="-p 7900:7900 -e SE_VNC_NO_PASSWORD=1 -e VNC_NO_PASSWORD=1"
+            fi
+            APACHE_OPTIONS="-e APACHE_RUN_USER=#${HOST_UID} -e APACHE_RUN_SERVERNAME=web -e APACHE_RUN_GROUP=#${HOST_PID} -e APACHE_RUN_DOCROOT=${CORE_ROOT}/typo3temp/var/tests/acceptance-composer/public -e PHPFPM_HOST=phpfpm -e PHPFPM_PORT=9000"
+            ${CONTAINER_BIN} run --rm ${CI_PARAMS} -d ${SELENIUM_GRID} --name ac-chrome-${SUFFIX} --network ${NETWORK} --network-alias chrome --tmpfs /dev/shm:rw,nosuid,nodev,noexec ${IMAGE_SELENIUM} >/dev/null
+            if [ ${CONTAINER_BIN} = "docker" ]; then
+                ${CONTAINER_BIN} run --rm -d --name ac-phpfpm-${SUFFIX} --network ${NETWORK} --network-alias phpfpm --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -e PHPFPM_USER=${HOST_UID} -e PHPFPM_GROUP=${HOST_PID} -v ${CORE_ROOT}:${CORE_ROOT} ${IMAGE_PHP} php-fpm ${PHP_FPM_OPTIONS} >/dev/null
+                ${CONTAINER_BIN} run --rm -d --name ac-web-${SUFFIX} --network ${NETWORK} --network-alias web --add-host "${CONTAINER_HOST}:host-gateway" -v ${CORE_ROOT}:${CORE_ROOT} ${APACHE_OPTIONS} ${IMAGE_APACHE} >/dev/null
+            else
+                ${CONTAINER_BIN} run --rm ${CI_PARAMS} -d --name ac-phpfpm-${SUFFIX} --network ${NETWORK} --network-alias phpfpm ${USERSET} -e PHPFPM_USER=0 -e PHPFPM_GROUP=0 -v ${CORE_ROOT}:${CORE_ROOT} ${IMAGE_PHP} php-fpm -R ${PHP_FPM_OPTIONS} >/dev/null
+                ${CONTAINER_BIN} run --rm ${CI_PARAMS} -d --name ac-web-${SUFFIX} --network ${NETWORK} --network-alias web -v ${CORE_ROOT}:${CORE_ROOT} ${APACHE_OPTIONS} ${IMAGE_APACHE} >/dev/null
+            fi
+            waitFor chrome 4444
+            waitFor chrome 7900
+            waitFor web 80
+            if [ "${ACCEPTANCE_HEADLESS}" -eq 0 ] && type "xdg-open" >/dev/null; then
+                xdg-open http://localhost:7900/?autoconnect=1 >/dev/null
+            elif [ "${ACCEPTANCE_HEADLESS}" -eq 0 ] && type "open" >/dev/null; then
+                open http://localhost:7900/?autoconnect=1 >/dev/null
+            fi
+
+            ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name ac-${DBMS}-composer ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${TESTPARAMS} ${IMAGE_PHP} "${COMMAND[@]}"
+            SUITE_EXIT_CODE=$?
+        fi
+        ;;
     acceptanceInstall)
         SELENIUM_GRID=""
         if [ "${ACCEPTANCE_HEADLESS}" -eq 0 ]; then
@@ -1009,7 +1079,7 @@ echo "##########################################################################
 echo "Result of ${TEST_SUITE}" >&2
 echo "Container runtime: ${CONTAINER_BIN}" >&2
 echo "PHP: ${PHP_VERSION}" >&2
-if [[ ${TEST_SUITE} =~ ^(functional|functionalDeprecated|acceptance|acceptanceInstall)$ ]]; then
+if [[ ${TEST_SUITE} =~ ^(functional|functionalDeprecated|acceptance|acceptanceComposer|acceptanceInstall)$ ]]; then
     case "${DBMS}" in
         mariadb|mysql|postgres)
             echo "DBMS: ${DBMS}  version ${DBMS_VERSION}  driver ${DATABASE_DRIVER}" >&2
diff --git a/Build/Scripts/setupAcceptanceComposer.sh b/Build/Scripts/setupAcceptanceComposer.sh
new file mode 100755
index 000000000000..5e5f43b9375b
--- /dev/null
+++ b/Build/Scripts/setupAcceptanceComposer.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+set -e
+
+cd "$(dirname $(realpath $0))/../../"
+
+PROJECT_PATH=${1:-typo3temp/var/tests/acceptance-composer/}
+export TYPO3_DB_DRIVER=${2:-${TYPO3_DB_DRIVER:-sqlite}}
+EXTRA_PACKAGES="${3}"
+
+mkdir -p "${PROJECT_PATH}"
+ln -snf $(echo "${PROJECT_PATH}" | sed -e 's/[^\/][^\/]*/../g' -e 's/\/$//')/typo3/sysext "${PROJECT_PATH}/typo3-sysext"
+ln -snf $(echo "${PROJECT_PATH}" | sed -e 's/[^\/][^\/]*/../g' -e 's/\/$//')/Build/tests/packages "${PROJECT_PATH}/packages"
+sed 's/..\/..\/typo3\/sysext/typo3-sysext/' Build/composer/composer.dist.json > "${PROJECT_PATH}/composer.json"
+
+cd "${PROJECT_PATH}"
+rm -rf composer.lock config/ public/ var/ vendor/
+
+mkdir -p "config/system/"
+cat > "config/system/additional.php" <<\EOF
+<?php
+$GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] = true;
+// "temporary password"
+$GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] = '$argon2i$v=19$m=65536,t=16,p=1$Rk9Edk1UWTd1MUtVY1Nydg$bJJgiAH3NT66LkvcTsnYbQvFS/ePOw/50rYjhxUk8L8';
+$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = true;
+$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
+$GLOBALS['TYPO3_CONF_VARS']['SYS']['exceptionalErrors'] = E_ALL;
+$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] = E_ALL;
+$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = '.*';
+$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor'] = 'GraphicsMagick';
+$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'mbox';
+$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file'] = \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/mail.mbox';
+EOF
+
+# `composer require` will implicitly perform an initial `composer install` since there is no composer.lock
+composer require --no-progress --no-interaction --dev typo3tests/dataset-import:@dev typo3/testing-framework:dev-main ${EXTRA_PACKAGES}
+
+TYPO3_SERVER_TYPE=apache \
+TYPO3_PROJECT_NAME="New TYPO3 site" \
+vendor/bin/typo3 setup --force --no-interaction
+
+vendor/bin/typo3 dataset:import vendor/typo3/cms-core/Tests/Acceptance/Fixtures/BackendEnvironment.csv
+vendor/bin/typo3 styleguide:generate -c -- all
+
+# Create favicon.ico to suppress potential javascript errors in console
+# which are caused by calling a non html in the browser, e.g. seo sitemap xml
+ln -snf ../vendor/typo3/cms-backend/Resources/Public/Icons/favicon.ico public/favicon.ico
+
+# @todo: needed for ugly InstallTool tests, that should be replace by a CLI command that properly enables install tool, both in composer and classic mode
+mkdir -p var/transient/
diff --git a/Build/composer/composer.dist.json b/Build/composer/composer.dist.json
index 1b4f727044a4..14960b281b7a 100644
--- a/Build/composer/composer.dist.json
+++ b/Build/composer/composer.dist.json
@@ -49,7 +49,8 @@
 					"typo3/cms-sys-note": "13.1.x-dev",
 					"typo3/cms-t3editor": "13.1.x-dev",
 					"typo3/cms-tstemplate": "13.1.x-dev",
-					"typo3/cms-viewpage": "13.1.x-dev"
+					"typo3/cms-viewpage": "13.1.x-dev",
+					"typo3/cms-workspaces": "13.1.x-dev"
 				}
 			}
 		},
@@ -92,6 +93,7 @@
 		"typo3/cms-sys-note": "@dev",
 		"typo3/cms-t3editor": "@dev",
 		"typo3/cms-tstemplate": "@dev",
-		"typo3/cms-viewpage": "@dev"
+		"typo3/cms-viewpage": "@dev",
+		"typo3/cms-workspaces": "@dev"
 	}
 }
diff --git a/Build/gitlab-ci.yml b/Build/gitlab-ci.yml
index ec1bdfbc1609..cac1d25b70ad 100644
--- a/Build/gitlab-ci.yml
+++ b/Build/gitlab-ci.yml
@@ -41,6 +41,7 @@ include:
   # turns this into a branch 'change-patchset' which executes the pipeline
   - local: 'Build/gitlab-ci/pre-merge/acceptance-install.yml'
   - local: 'Build/gitlab-ci/pre-merge/acceptance-application.yml'
+  - local: 'Build/gitlab-ci/pre-merge/acceptance-application-composer.yml'
   - local: 'Build/gitlab-ci/pre-merge/integrity.yml'
   - local: 'Build/gitlab-ci/pre-merge/functional.yml'
   - local: 'Build/gitlab-ci/pre-merge/unit.yml'
@@ -49,4 +50,5 @@ include:
   - local: 'Build/gitlab-ci/nightly/unit.yml'
   - local: 'Build/gitlab-ci/nightly/acceptance-install.yml'
   - local: 'Build/gitlab-ci/nightly/acceptance-application.yml'
+  - local: 'Build/gitlab-ci/nightly/acceptance-application-composer.yml'
   - local: 'Build/gitlab-ci/nightly/functional.yml'
diff --git a/Build/gitlab-ci/nightly/acceptance-application-composer.yml b/Build/gitlab-ci/nightly/acceptance-application-composer.yml
new file mode 100644
index 000000000000..164d8df903ad
--- /dev/null
+++ b/Build/gitlab-ci/nightly/acceptance-application-composer.yml
@@ -0,0 +1,163 @@
+acceptance application composer mariadb 10.4 php 8.2 min:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-min
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMin -p 8.2
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d mariadb -i 10.4 -p 8.2 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+acceptance application composer mariadb 10.10 php 8.3 max:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-max
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMax -p 8.3
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d mariadb -i 10.10 -p 8.3 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+
+acceptance application composer mysql 8.0 php 8.3 max:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-max
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMax -p 8.3
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d mysql -i 8.0 -p 8.3 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+acceptance application composer mysql 8.0 php 8.2 min:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-min
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMin -p 8.2
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d mysql -i 8.0 -p 8.2 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+
+acceptance application composer sqlite php 8.3 max:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-max
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMax -p 8.3
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d sqlite -p 8.3 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+acceptance application composer sqlite php 8.2 min:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-min
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMin -p 8.2
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d sqlite -p 8.2 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+
+acceptance application composer postgres 15 php 8.3 max:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-max
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMax -p 8.3
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d postgres -i 15 -p 8.3 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
+acceptance application composer postgres 10 php 8.2 min:
+  stage: acceptance
+  tags:
+    - metal2
+  needs: []
+  only:
+    - schedules
+  cache:
+    key: main-composer-min
+    paths:
+      - .cache
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 8
+  script:
+    - Build/Scripts/runTests.sh -s composerInstallMin -p 8.2
+    - Build/Scripts/runTests.sh -s acceptanceComposer -d postgres -i 10 -p 8.2 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
diff --git a/Build/gitlab-ci/pre-merge/acceptance-application-composer.yml b/Build/gitlab-ci/pre-merge/acceptance-application-composer.yml
new file mode 100644
index 000000000000..f3d68f7de5f7
--- /dev/null
+++ b/Build/gitlab-ci/pre-merge/acceptance-application-composer.yml
@@ -0,0 +1,17 @@
+acceptance application composer postgres 15 php 8.2 pre-merge:
+  stage: main
+  tags:
+    - metal2
+  except:
+    refs:
+      - schedules
+      - main
+  artifacts:
+    when: on_failure
+    paths:
+      - typo3temp/var/tests/acceptance-composer/var/log
+      - typo3temp/var/tests/AcceptanceReports
+  parallel: 13
+  script:
+    - Build/Scripts/runTests.sh -s composerInstall -p 8.2
+    - Build/Scripts/runTests.sh -s acceptanceComposer -p 8.2 -d postgres -i 15 -c $CI_NODE_INDEX/$CI_NODE_TOTAL
diff --git a/Build/tests/packages/dataset_import/Classes/Command/DatasetImportCommand.php b/Build/tests/packages/dataset_import/Classes/Command/DatasetImportCommand.php
new file mode 100644
index 000000000000..eb51b6457e93
--- /dev/null
+++ b/Build/tests/packages/dataset_import/Classes/Command/DatasetImportCommand.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3Tests\DatasetImport\Command;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\DataSet;
+
+/**
+ * CLI command for setting up TYPO3 via CLI
+ */
+#[AsCommand('dataset:import', 'Import Dataset')]
+class DatasetImportCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->addArgument('path', InputArgument::REQUIRED, 'Path to CSV dataset to import');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        if (!class_exists(DataSet::class)) {
+            $io = new SymfonyStyle($input, $output);
+            $io->getErrorStyle()->error('Missing typo3/testing-framework dependency.');
+            return Command::FAILURE;
+        }
+        DataSet::import($input->getArgument('path'));
+        return Command::SUCCESS;
+    }
+
+}
diff --git a/Build/tests/packages/dataset_import/Configuration/Services.yaml b/Build/tests/packages/dataset_import/Configuration/Services.yaml
new file mode 100644
index 000000000000..adae8cd8e361
--- /dev/null
+++ b/Build/tests/packages/dataset_import/Configuration/Services.yaml
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3Tests\DatasetImport\:
+    resource: '../Classes/*'
diff --git a/Build/tests/packages/dataset_import/composer.json b/Build/tests/packages/dataset_import/composer.json
new file mode 100644
index 000000000000..9f1d4b403404
--- /dev/null
+++ b/Build/tests/packages/dataset_import/composer.json
@@ -0,0 +1,20 @@
+{
+	"name": "typo3tests/dataset-import",
+	"type": "typo3-cms-extension",
+	"description": "Support extension providing dataset:import command",
+	"license": "GPL-2.0-or-later",
+	"require": {
+		"typo3/cms-core": "13.1.*@dev",
+		"typo3/testing-framework": "dev-main"
+	},
+	"extra": {
+		"typo3/cms": {
+			"extension-key": "dataset_import"
+		}
+	},
+	"autoload": {
+		"psr-4": {
+			"TYPO3Tests\\DatasetImport\\": "Classes/"
+		}
+	}
+}
diff --git a/typo3/sysext/core/Tests/Acceptance/Application.suite.yml b/typo3/sysext/core/Tests/Acceptance/Application.suite.yml
index 4ccfcfbef30e..d4e288724934 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application.suite.yml
+++ b/typo3/sysext/core/Tests/Acceptance/Application.suite.yml
@@ -16,10 +16,14 @@ modules:
         editor: '%typo3TestingAcceptanceEditorPassword%'
 
 env:
-  ci:
+  classic:
     extensions:
       enabled:
         - TYPO3\CMS\Core\Tests\Acceptance\Support\Extension\ApplicationEnvironment
+  composer:
+    extensions:
+      enabled:
+        - TYPO3\CMS\Core\Tests\Acceptance\Support\Extension\ApplicationComposerEnvironment
 
 groups:
   AcceptanceTests-Job-*: AcceptanceTests-Job-*
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/BackendUser/ListUserCest.php b/typo3/sysext/core/Tests/Acceptance/Application/BackendUser/ListUserCest.php
index 910bf40fd0cc..57b66fc726cb 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/BackendUser/ListUserCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/BackendUser/ListUserCest.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\BackendUser;
 
+use Codeception\Scenario;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 
 /**
@@ -34,23 +35,38 @@ final class ListUserCest
         $I->switchToContentFrame();
     }
 
-    public function showsHeadingAndListsBackendUsers(ApplicationTester $I): void
+    public function showsHeadingAndListsBackendUsers(ApplicationTester $I, Scenario $scenario): void
     {
         $I->see('Backend users');
 
         $I->wantTo('See the table of users');
         $I->waitForElementVisible('#typo3-backend-user-list');
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
 
-        // We expect exact four Backend Users created from the Fixtures
-        $this->checkCountOfUsers($I, 4);
+        $isComposerMode = str_contains($scenario->current('env'), 'composer');
+        // We expect exactly four Backend Users to have been created by the fixtures
+        $expectedUsers = 4;
+        if ($isComposerMode) {
+            // User _cli_ will additionally be available in composer mode, created
+            // by execution of `vendor/bin/typo3` CLI in setup script.
+            $expectedUsers++;
+        }
+        $this->checkCountOfUsers($I, $expectedUsers);
     }
 
-    public function filterUsersByUsername(ApplicationTester $I): void
+    public function filterUsersByUsername(ApplicationTester $I, Scenario $scenario): void
     {
         $I->wantTo('See the table of users');
         $I->waitForElementVisible('#typo3-backend-user-list');
-        // We expect exact four Backend Users created from the Fixtures
-        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', 4);
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $isComposerMode = str_contains($scenario->current('env'), 'composer');
+        $expectedUsers = 4;
+        if ($isComposerMode) {
+            $expectedUsers++;
+        }
+        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', $expectedUsers);
 
         $I->wantTo('Filter the list of user by valid username admin');
         $I->fillField('#tx_Beuser_username', 'admin');
@@ -71,12 +87,18 @@ final class ListUserCest
         $this->checkCountOfUsers($I, 0);
     }
 
-    public function filterUsersByAdmin(ApplicationTester $I): void
+    public function filterUsersByAdmin(ApplicationTester $I, Scenario $scenario): void
     {
         $I->wantTo('See the table of users');
         $I->waitForElementVisible('#typo3-backend-user-list');
-        // We expect exact four Backend Users created from the Fixtures
-        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', 4);
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $isComposerMode = str_contains($scenario->current('env'), 'composer');
+        $expectedUsers = 4;
+        if ($isComposerMode) {
+            $expectedUsers++;
+        }
+        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', $expectedUsers);
 
         $I->wantToTest('Filter BackendUser and see only admins');
         $I->selectOption('#tx_Beuser_usertype', 'Admin only');
@@ -84,8 +106,8 @@ final class ListUserCest
         $I->waitForElementNotVisible('div#nprogess');
         $I->waitForElementVisible('#typo3-backend-user-list');
 
-        // We expect exact two fitting Backend Users created from the Fixtures
-        $this->checkCountOfUsers($I, 2);
+        // We expect exact two (composer-mode: three) fitting Backend Users created from the Fixtures
+        $this->checkCountOfUsers($I, 2 + ($isComposerMode ? 1 : 0));
 
         $I->wantToTest('Filter BackendUser and see normal users');
         $I->selectOption('#tx_Beuser_usertype', 'Normal users only');
@@ -97,12 +119,18 @@ final class ListUserCest
         $this->checkCountOfUsers($I, 2);
     }
 
-    public function filterUsersByStatus(ApplicationTester $I): void
+    public function filterUsersByStatus(ApplicationTester $I, Scenario $scenario): void
     {
         $I->wantTo('See the table of users');
         $I->waitForElementVisible('#typo3-backend-user-list');
-        // We expect exact four Backend Users created from the Fixtures
-        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', 4);
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $isComposerMode = str_contains($scenario->current('env'), 'composer');
+        $expectedUsers = 4;
+        if ($isComposerMode) {
+            $expectedUsers++;
+        }
+        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', $expectedUsers);
 
         $I->wantToTest('Filter BackendUser and see only active users');
         $I->selectOption('#tx_Beuser_status', 'Active only');
@@ -110,8 +138,8 @@ final class ListUserCest
         $I->waitForElementNotVisible('div#nprogess');
         $I->waitForElementVisible('#typo3-backend-user-list');
 
-        // We expect exact two fitting Backend Users created from the Fixtures
-        $this->checkCountOfUsers($I, 2);
+        // We expect exact two (composer-mode three) fitting Backend Users created from the Fixtures
+        $this->checkCountOfUsers($I, 2 + ($isComposerMode ? 1 : 0));
 
         $I->wantToTest('Filter BackendUser and see only inactive users');
         $I->selectOption('#tx_Beuser_status', 'Inactive only');
@@ -123,12 +151,18 @@ final class ListUserCest
         $this->checkCountOfUsers($I, 2);
     }
 
-    public function filterUsersByLogin(ApplicationTester $I): void
+    public function filterUsersByLogin(ApplicationTester $I, Scenario $scenario): void
     {
         $I->wantTo('See the table of users');
         $I->waitForElementVisible('#typo3-backend-user-list');
-        // We expect exact four Backend Users created from the Fixtures
-        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', 4);
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $isComposerMode = str_contains($scenario->current('env'), 'composer');
+        $expectedUsers = 4;
+        if ($isComposerMode) {
+            $expectedUsers++;
+        }
+        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', $expectedUsers);
 
         $I->wantToTest('Filter BackendUser and see only users logged in before');
         $I->selectOption('#tx_Beuser_logins', 'Logged in before');
@@ -145,16 +179,22 @@ final class ListUserCest
         $I->waitForElementNotVisible('div#nprogess');
         $I->waitForElementVisible('#typo3-backend-user-list');
 
-        // We expect exact two fitting Backend Users created from the Fixtures
-        $this->checkCountOfUsers($I, 2);
+        // We expect exact two (composer-mode three) fitting Backend Users created from the Fixtures
+        $this->checkCountOfUsers($I, 2 + ($isComposerMode ? 1 : 0));
     }
 
-    public function filterUsersByUserGroup(ApplicationTester $I): void
+    public function filterUsersByUserGroup(ApplicationTester $I, Scenario $scenario): void
     {
         $I->wantTo('See the table of users');
         $I->waitForElementVisible('#typo3-backend-user-list');
-        // We expect exact four Backend Users created from the Fixtures
-        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', 4);
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $isComposerMode = str_contains($scenario->current('env'), 'composer');
+        $expectedUsers = 4;
+        if ($isComposerMode) {
+            $expectedUsers++;
+        }
+        $I->canSeeNumberOfElements('#typo3-backend-user-list tbody tr', $expectedUsers);
 
         // We expect exact one Backend Users created from the Fixtures has the usergroup named 'editor-group'
         $I->wantToTest('Filter BackendUser and see only users with given usergroup');
@@ -170,18 +210,22 @@ final class ListUserCest
     public function canEditUsersFromIndexListView(ApplicationTester $I): void
     {
         $I->canSee('Backend users', 'h1');
-        $username = $I->grabTextFrom('#typo3-backend-user-list > tbody > tr:nth-child(1) > td.col-title > a:nth-child(1) > b');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $I->click('button[value="reset-filters"]');
+        $I->waitForElementVisible('#typo3-backend-user-list');
+        $username = 'admin';
+        $adminRow = '//*[@id="typo3-backend-user-list"]//tr[contains(td[2]/a[1]/b[1], "' . $username . '")]';
 
         $I->amGoingTo('test the edit button');
-        $I->click('#typo3-backend-user-list > tbody > tr:nth-child(1) > td.col-control > div:nth-child(1) > a');
+        $I->click($adminRow . '//div[@role="group"]/a[@title="Edit"]');
         $this->openAndCloseTheEditForm($I, $username);
 
         $I->amGoingTo('test the edit link on username');
-        $I->click('#typo3-backend-user-list > tbody > tr:nth-child(1) > td.col-title > a:nth-child(1)');
+        $I->click($adminRow . '//td[@class="col-title"]/a[1]');
         $this->openAndCloseTheEditForm($I, $username);
 
         $I->amGoingTo('test the edit link on real name');
-        $I->click('#typo3-backend-user-list > tbody > tr:nth-child(1) > td.col-title > a:nth-child(4)');
+        $I->click($adminRow . '//td[@class="col-title"]/a[2]');
         $this->openAndCloseTheEditForm($I, $username);
     }
 
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php
index aec2d2505dc5..89dbbaa169fb 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Extensionmanager;
 
+use Codeception\Attribute\Env;
 use Facebook\WebDriver\WebDriverKeys;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 
@@ -47,18 +48,21 @@ final class GetExtensionsCest
         $I->seeNumberOfElements('#terTable tbody tr', 2);
     }
 
+    #[Env('classic')]
     public function checkRetrievedExtensionsFromTerAreDisplayed(ApplicationTester $I): void
     {
         $I->see('superext');
         $I->see('neededext');
     }
 
+    #[Env('classic')]
     public function checkPaginationIsNotDisplayedForTwoRecords(ApplicationTester $I): void
     {
         $I->dontSeeElement('.pagination-wrap');
         $I->dontSee('Extensions 1 - 2');
     }
 
+    #[Env('classic')]
     public function checkSearchFilterListFindsExtensionKey(ApplicationTester $I): void
     {
         $I->fillField('input[name="search"]', 'superext');
@@ -81,6 +85,7 @@ final class GetExtensionsCest
         $I->see('Needed Extension');
     }
 
+    #[Env('classic')]
     public function checkSearchFilterListFindsPartOfExtensionKey(ApplicationTester $I): void
     {
         $I->fillField('input[name="search"]', 'ext');
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/InstalledExtensionsCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/InstalledExtensionsCest.php
index c3aee7e30146..915b3d47632d 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/InstalledExtensionsCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/InstalledExtensionsCest.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Extensionmanager;
 
+use Codeception\Attribute\Env;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 
 /**
@@ -61,6 +62,7 @@ final class InstalledExtensionsCest
         $I->seeNumberOfElements('#typo3-extension-list tbody tr[role="row"]', [10, 100]);
     }
 
+    #[Env('classic')]
     public function checkIfUploadFormAppears(ApplicationTester $I): void
     {
         $I->cantSeeElement('.module-body .extension-upload-form');
@@ -68,6 +70,7 @@ final class InstalledExtensionsCest
         $I->seeElement('.module-body .extension-upload-form');
     }
 
+    #[Env('classic')]
     public function checkUninstallingAndInstallingAnExtension(ApplicationTester $I): void
     {
         $I->wantTo('Check if uninstalling and installing an extension with backend module removes and adds the module from the module menu.');
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/FalMetadataCest.php b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/FalMetadataCest.php
index f541dcdf772e..a6b31ccb64cc 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/FalMetadataCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/FalMetadataCest.php
@@ -108,7 +108,10 @@ final class FalMetadataCest
         $I->waitForElementNotVisible('#t3js-ui-block');
         $I->waitForText('Edit Page Content "tt_content with image" on page "styleguide TCA demo"');
         $I->click('Images');
-        $I->click('.form-irre-header');
+        if (count($I->grabMultiple('.panel-collapsed .form-irre-header')) > 0) {
+            $I->click('.panel-collapsed .form-irre-header');
+        }
+        $I->waitForElement('.t3js-form-field-eval-null-placeholder-checkbox');
 
         $I->see('(Default: "Test title")', '.t3js-form-field-eval-null-placeholder-checkbox');
         $I->see('(Default: "Test alternative")', '.t3js-form-field-eval-null-placeholder-checkbox');
@@ -203,7 +206,10 @@ final class FalMetadataCest
         $I->waitForElementNotVisible('#t3js-ui-block');
         $I->waitForText('Edit Page Content "tt_content with image" on page "styleguide TCA demo"');
         $I->click('Images');
-        $I->click('.form-irre-header');
+        if (count($I->grabMultiple('.panel-collapsed .form-irre-header')) > 0) {
+            $I->click('.panel-collapsed .form-irre-header');
+        }
+        $I->waitForElement('.t3js-form-field-eval-null-placeholder-checkbox');
 
         $I->see('(Default: "Test title")', '.t3js-form-field-eval-null-placeholder-checkbox');
         $I->see('(Default: "Test alternative")', '.t3js-form-field-eval-null-placeholder-checkbox');
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/Inline1nCest.php b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/Inline1nCest.php
index 8128e2e91203..92b92d22d9ce 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/Inline1nCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/Inline1nCest.php
@@ -89,6 +89,11 @@ final class Inline1nCest
 
         $I->see('lipsum', '#recordlist-tx_styleguide_inline_1n_inline_1_child > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(3) > a');
         $I->see('Fo Bar', '#recordlist-tx_styleguide_inline_1n_inline_1_child > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(2) > tr:nth-child(4) > td:nth-child(3) > a');
+
+        $I->click('button[data-table="tx_styleguide_inline_1n"] .icon-actions-view-list-expand');
+        $I->wait(1);
+        $I->click('button[data-table="pages_translated"] .icon-actions-view-list-expand');
+        $I->wait(1);
     }
 
     /**
@@ -111,6 +116,11 @@ final class Inline1nCest
         $I->wantTo('Check new sorting');
         $I->see('Fo Bar', '#recordlist-tx_styleguide_inline_1n_inline_1_child > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(3) > a');
         $I->see('lipsum', '#recordlist-tx_styleguide_inline_1n_inline_1_child > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(2) > tr:nth-child(2) > td:nth-child(3) > a');
+
+        $I->click('button[data-table="tx_styleguide_inline_1n"] .icon-actions-view-list-expand');
+        $I->wait(1);
+        $I->click('button[data-table="pages_translated"] .icon-actions-view-list-expand');
+        $I->wait(1);
     }
 
     public function changeInline1nInlineInput(ApplicationTester $I): void
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/ContentElementsCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/ContentElementsCest.php
index 7e0fdf5552d9..43580c49874e 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/ContentElementsCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/ContentElementsCest.php
@@ -31,6 +31,8 @@ final class ContentElementsCest
         $I->click('Page');
         $pageTree->openPath(['styleguide frontend demo']);
         $I->switchToContentFrame();
+        $I->waitForElementVisible('select[name=actionMenu]');
+        $I->selectOption('select[name=actionMenu]', 'Layout');
         $I->waitForElementVisible('.t3js-module-docheader-bar a[title="View webpage"]');
         $I->wait(1);
         $I->click('.t3js-module-docheader-bar a[title="View webpage"]');
@@ -49,9 +51,13 @@ final class ContentElementsCest
         // Close FE tab again and switch to BE to avoid side effects
         $I->executeInSelenium(static function (RemoteWebDriver $webdriver) {
             $handles = $webdriver->getWindowHandles();
-            $webdriver->close();
-            $firstWindow = current($handles);
-            $webdriver->switchTo()->window($firstWindow);
+            // Avoid closing the main backend tab (holds the webdriver session) if the test failed to open the frontend tab
+            // (All subsequent tests would fail with "[Facebook\WebDriver\Exception\InvalidSessionIdException] invalid session id"
+            if (count($handles) > 1) {
+                $webdriver->close();
+                $firstWindow = current($handles);
+                $webdriver->switchTo()->window($firstWindow);
+            }
         });
     }
 
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FormFrameworkCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FormFrameworkCest.php
index 370fefff61cb..e04b2dfc765e 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FormFrameworkCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FormFrameworkCest.php
@@ -37,6 +37,8 @@ final class FormFrameworkCest
         $I->click('Page');
         $pageTree->openPath(['styleguide frontend demo']);
         $I->switchToContentFrame();
+        $I->waitForElementVisible('select[name=actionMenu]');
+        $I->selectOption('select[name=actionMenu]', 'Layout');
         $I->waitForElementVisible('.t3js-module-docheader-bar a[title="View webpage"]');
         $I->wait(1);
         $I->click('.t3js-module-docheader-bar a[title="View webpage"]');
@@ -57,9 +59,13 @@ final class FormFrameworkCest
         // Close FE tab again and switch to BE to avoid side effects
         $I->executeInSelenium(static function (RemoteWebDriver $webdriver) {
             $handles = $webdriver->getWindowHandles();
-            $webdriver->close();
-            $firstWindow = current($handles);
-            $webdriver->switchTo()->window($firstWindow);
+            // Avoid closing the main backend tab (holds the webdriver session) if the test failed to open the frontend tab
+            // (All subsequent tests would fail with "[Facebook\WebDriver\Exception\InvalidSessionIdException] invalid session id"
+            if (count($handles) > 1) {
+                $webdriver->close();
+                $firstWindow = current($handles);
+                $webdriver->switchTo()->window($firstWindow);
+            }
         });
     }
 
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FrontendLoginCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FrontendLoginCest.php
index 30f2e0f3a0a0..03597fa69921 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FrontendLoginCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FrontendLoginCest.php
@@ -35,6 +35,8 @@ final class FrontendLoginCest
         $I->click('Page');
         $pageTree->openPath(['styleguide frontend demo']);
         $I->switchToContentFrame();
+        $I->waitForElementVisible('select[name=actionMenu]');
+        $I->selectOption('select[name=actionMenu]', 'Layout');
         $I->waitForElementVisible('.t3js-module-docheader-bar a[title="View webpage"]');
         $I->wait(1);
         $I->click('.t3js-module-docheader-bar a[title="View webpage"]');
@@ -55,9 +57,13 @@ final class FrontendLoginCest
         // Close FE tab again and switch to BE to avoid side effects
         $I->executeInSelenium(static function (RemoteWebDriver $webdriver) {
             $handles = $webdriver->getWindowHandles();
-            $webdriver->close();
-            $firstWindow = current($handles);
-            $webdriver->switchTo()->window($firstWindow);
+            // Avoid closing the main backend tab (holds the webdriver session) if the test failed to open the frontend tab
+            // (All subsequent tests would fail with "[Facebook\WebDriver\Exception\InvalidSessionIdException] invalid session id"
+            if (count($handles) > 1) {
+                $webdriver->close();
+                $firstWindow = current($handles);
+                $webdriver->switchTo()->window($firstWindow);
+            }
         });
     }
 
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/IndexedSearchCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/IndexedSearchCest.php
index e61b74ec66e1..363a8878f2c3 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/IndexedSearchCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/IndexedSearchCest.php
@@ -36,6 +36,8 @@ final class IndexedSearchCest
         $I->click('Page');
         $pageTree->openPath(['styleguide frontend demo']);
         $I->switchToContentFrame();
+        $I->waitForElementVisible('select[name=actionMenu]');
+        $I->selectOption('select[name=actionMenu]', 'Layout');
         $I->waitForElementVisible('.t3js-module-docheader-bar a[title="View webpage"]');
         $I->wait(1);
         $I->click('.t3js-module-docheader-bar a[title="View webpage"]');
@@ -56,9 +58,13 @@ final class IndexedSearchCest
         // Close FE tab again and switch to BE to avoid side effects
         $I->executeInSelenium(static function (RemoteWebDriver $webdriver) {
             $handles = $webdriver->getWindowHandles();
-            $webdriver->close();
-            $firstWindow = current($handles);
-            $webdriver->switchTo()->window($firstWindow);
+            // Avoid closing the main backend tab (holds the webdriver session) if the test failed to open the frontend tab
+            // (All subsequent tests would fail with "[Facebook\WebDriver\Exception\InvalidSessionIdException] invalid session id"
+            if (count($handles) > 1) {
+                $webdriver->close();
+                $firstWindow = current($handles);
+                $webdriver->switchTo()->window($firstWindow);
+            }
         });
     }
 
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ImportCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ImportCest.php
index 7ec1bc16c6ef..c337096351c4 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ImportCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ImportCest.php
@@ -17,7 +17,6 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Impexp;
 
-use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\ModalDialog;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\PageTree;
@@ -143,7 +142,7 @@ final class ImportCest extends AbstractCest
         $I->canSeeElement($this->inModuleTabs . ' ' . $this->tabMessages);
         $flashMessage = $I->grabTextFrom($this->inFlashMessages . ' .alert.alert-success .alert-message');
         preg_match('/[^"]+"([^"]+)"[^"]+"([^"]+)"[^"]+/', $flashMessage, $flashMessageParts);
-        $loadFilePath = Environment::getProjectPath() . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
+        $loadFilePath = getenv('TYPO3_ACCEPTANCE_PATH_WEB') . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
         $I->assertFileExists($loadFilePath);
         $this->testFilesToDelete[] = $loadFilePath;
 
@@ -180,7 +179,7 @@ final class ImportCest extends AbstractCest
         $I->cantSeeElement($this->inModuleTabs . ' ' . $this->tabMessages);
         $flashMessage = $I->grabTextFrom($this->inFlashMessages . ' .alert.alert-success .alert-message');
         preg_match('/[^"]+"([^"]+)"[^"]+"([^"]+)"[^"]+/', $flashMessage, $flashMessageParts);
-        $loadFilePath = Environment::getProjectPath() . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
+        $loadFilePath = getenv('TYPO3_ACCEPTANCE_PATH_WEB') . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
         $I->assertFileExists($loadFilePath);
         $this->testFilesToDelete[] = $loadFilePath;
 
@@ -221,7 +220,7 @@ final class ImportCest extends AbstractCest
         $I->cantSeeElement($this->inModuleTabs . ' ' . $this->tabMessages);
         $flashMessage = $I->grabTextFrom($this->inFlashMessages . ' .alert.alert-success .alert-message');
         preg_match('/[^"]+"([^"]+)"[^"]+"([^"]+)"[^"]+/', $flashMessage, $flashMessageParts);
-        $loadFilePath = Environment::getProjectPath() . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
+        $loadFilePath = getenv('TYPO3_ACCEPTANCE_PATH_WEB') . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
         $I->assertFileExists($loadFilePath);
         $this->testFilesToDelete[] = $loadFilePath;
 
@@ -264,7 +263,7 @@ final class ImportCest extends AbstractCest
         $I->cantSeeElement($this->inModuleTabs . ' ' . $this->tabMessages);
         $flashMessage = $I->grabTextFrom($this->inFlashMessages . ' .alert.alert-success .alert-message');
         preg_match('/[^"]+"([^"]+)"[^"]+"([^"]+)"[^"]+/', $flashMessage, $flashMessageParts);
-        $loadFilePath = Environment::getProjectPath() . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
+        $loadFilePath = getenv('TYPO3_ACCEPTANCE_PATH_WEB') . '/fileadmin' . $flashMessageParts[2] . $flashMessageParts[1];
         $I->assertFileExists($loadFilePath);
         $this->testFilesToDelete[] = $loadFilePath;
 
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/AbstractCest.php b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/AbstractCest.php
index bee44be2fb93..cf832d38256e 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/AbstractCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/AbstractCest.php
@@ -17,7 +17,6 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\InstallTool;
 
-use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -25,7 +24,7 @@ use TYPO3\CMS\Install\Service\EnableFileService;
 
 class AbstractCest
 {
-    private const ADDITIONAL_CONFIGURATION_FILEPATH = 'typo3conf/system/additional.php';
+    private const ADDITIONAL_CONFIGURATION_FILEPATH = '/system/additional.php';
     protected const INSTALL_TOOL_PASSWORD = 'temporary password';
 
     public function _before(ApplicationTester $I): void
@@ -40,15 +39,19 @@ class AbstractCest
         $I->waitForText('The Install Tool is locked', 20);
 
         $I->amGoingTo('clean up created files');
-        unlink(Environment::getProjectPath() . '/' . self::ADDITIONAL_CONFIGURATION_FILEPATH);
+        if (getenv('TYPO3_ACCEPTANCE_INSTALLTOOL_PW_PRESET') !== '1') {
+            unlink(getenv('TYPO3_ACCEPTANCE_PATH_CONFIG') . self::ADDITIONAL_CONFIGURATION_FILEPATH);
+        }
 
         $I->dontSeeFileFound($this->getEnableInstallToolFilePath());
-        $I->dontSeeFileFound(Environment::getProjectPath() . '/' . self::ADDITIONAL_CONFIGURATION_FILEPATH);
+        if (getenv('TYPO3_ACCEPTANCE_INSTALLTOOL_PW_PRESET') !== '1') {
+            $I->dontSeeFileFound(getenv('TYPO3_ACCEPTANCE_PATH_CONFIG') . self::ADDITIONAL_CONFIGURATION_FILEPATH);
+        }
     }
 
     protected function getEnableInstallToolFilePath(): string
     {
-        return Environment::getVarPath() . '/transient/' . EnableFileService::INSTALL_TOOL_ENABLE_FILE_PATH;
+        return getenv('TYPO3_ACCEPTANCE_PATH_VAR') . '/transient/' . EnableFileService::INSTALL_TOOL_ENABLE_FILE_PATH;
     }
 
     protected function logIntoInstallTool(ApplicationTester $I): void
@@ -65,11 +68,14 @@ class AbstractCest
 
     private function setInstallToolPassword(ApplicationTester $I): string
     {
-        $hashMethod = GeneralUtility::makeInstance(Argon2iPasswordHash::class);
         $password = self::INSTALL_TOOL_PASSWORD;
+        if (getenv('TYPO3_ACCEPTANCE_INSTALLTOOL_PW_PRESET') === '1') {
+            return $password;
+        }
+        $hashMethod = GeneralUtility::makeInstance(Argon2iPasswordHash::class);
         $hashedPassword = $hashMethod->getHashedPassword($password);
         $I->writeToFile(
-            self::ADDITIONAL_CONFIGURATION_FILEPATH,
+            getenv('TYPO3_ACCEPTANCE_PATH_CONFIG') . self::ADDITIONAL_CONFIGURATION_FILEPATH,
             '<?php' . PHP_EOL . '$GLOBALS[\'TYPO3_CONF_VARS\'][\'BE\'][\'installToolPassword\'] = \'' . $hashedPassword . '\';'
         );
         return $password;
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/MaintenanceCest.php b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/MaintenanceCest.php
index 677ab3c0eab3..4e6ac683c183 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/MaintenanceCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/MaintenanceCest.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\InstallTool;
 
+use Codeception\Attribute\Env;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 
 final class MaintenanceCest extends AbstractCest
@@ -56,6 +57,7 @@ final class MaintenanceCest extends AbstractCest
         $I->waitForElementNotVisible('.modal-dialog');
     }
 
+    #[Env('classic')]
     public function dumpAutoloadWorks(ApplicationTester $I): void
     {
         $I->click('Dump autoload');
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/SettingsCest.php b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/SettingsCest.php
index 882f1af6d1d3..50b30206d13f 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/SettingsCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/SettingsCest.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\InstallTool;
 
+use Codeception\Scenario;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\ModalDialog;
 
@@ -151,7 +152,7 @@ final class SettingsCest extends AbstractCest
         $this->closeModalAndHideFlashMessage($I);
     }
 
-    public function seeFeatureToggles(ApplicationTester $I, ModalDialog $modalDialog): void
+    public function seeFeatureToggles(ApplicationTester $I, ModalDialog $modalDialog, Scenario $scenario): void
     {
         $button = 'Configure Features…';
         $modalButton = 'Save';
@@ -169,7 +170,12 @@ final class SettingsCest extends AbstractCest
         // Switch back hit count feature toggle
         $I->click($button);
         $modalDialog->canSeeDialog();
-        $I->cantSeeCheckboxIsChecked($featureToggle);
+        if (str_contains($scenario->current('env'), 'classic')) {
+            // ['features']['redirects.hitCount'] is enabled by default in classic mode (set by TF BackendEnvironment setup)
+            $I->cantSeeCheckboxIsChecked($featureToggle);
+        } else {
+            $I->canSeeCheckboxIsChecked($featureToggle);
+        }
         $I->amGoingTo('reset hit count feature toggle and save it');
         $I->click($featureToggle);
         $I->click($modalButton, ModalDialog::$openedModalButtonContainerSelector);
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/UpgradeCest.php b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/UpgradeCest.php
index 997c4107a82c..816a05b9a223 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/UpgradeCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/InstallTool/UpgradeCest.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Acceptance\Application\InstallTool;
 
+use Codeception\Attribute\Env;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
 use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\ModalDialog;
 
@@ -30,6 +31,7 @@ final class UpgradeCest extends AbstractCest
         $I->see('Upgrade', 'h1');
     }
 
+    #[Env('classic')]
     public function seeUpgradeCore(ApplicationTester $I, ModalDialog $modalDialog): void
     {
         $I->click('Update Core…');
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Redirect/RedirectModuleCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Redirect/RedirectModuleCest.php
index b219db8a3dc7..6f48e185c9d6 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Redirect/RedirectModuleCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Redirect/RedirectModuleCest.php
@@ -71,7 +71,7 @@ final class RedirectModuleCest
         $I->canSee('Redirect Management', 'h1');
 
         $I->amGoingTo('test edit on edit button');
-        $I->click('table.table-striped > tbody > tr > td:nth-child(8) > div > a:nth-child(2)');
+        $I->click('table.table-striped > tbody > tr > td.col-control > div > a:nth-child(2)');
         $I->waitForElementNotVisible('#t3js-ui-block');
         $I->canSee('Edit Redirect "' . $sourceHost . ', ' . $sourcePath . '" on root level');
         $I->click('div.module-docheader .btn.t3js-editform-close');
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php
index f6ac098e002a..8be04e089a78 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php
@@ -126,6 +126,7 @@ final class TasksCest
         $I->waitForElementVisible('[data-module-name="scheduler_availabletasks"]');
         $I->see('Available scheduler commands & tasks');
         $I->canSeeNumberOfElements('[data-module-name="scheduler_availabletasks"] table tbody tr', [1, 10000]);
+        $I->selectOption('select[name=moduleMenu]', 'Scheduled tasks');
     }
 
     public function canCreateNewTaskGroupFromEditForm(ApplicationTester $I, ModalDialog $modalDialog): void
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php
index bc37fa0239c5..b33275258b89 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php
@@ -166,6 +166,10 @@ final class TemplateCest
         $I->waitForText('Edit the whole TypoScript record');
         $I->see('Edit the whole TypoScript record');
         $I->click('Edit the whole TypoScript record');
+        // Avoid race condition:
+        // SEVERE - http://web/typo3/sysext/backend/Resources/Public/JavaScript/code-editor/autocomplete/ts-ref.js?bust=[…]
+        // 12:613 Uncaught TypeError: Cannot convert undefined or null to object
+        $I->waitForElementNotVisible('#nprogress', 120);
     }
 
     public function createExtensionTemplate(ApplicationTester $I, PageTree $pageTree): void
diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationComposerEnvironment.php b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationComposerEnvironment.php
new file mode 100644
index 000000000000..2a19602a628d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationComposerEnvironment.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Acceptance\Support\Extension;
+
+use Codeception\Events;
+use Codeception\Extension;
+
+/**
+ * @internal Used by core, do not use in extensions, may vanish later.
+ */
+final class ApplicationComposerEnvironment extends Extension
+{
+    /**
+     * @var array Default configuration values
+     */
+    protected array $config = [
+        'typo3InstancePath' => 'typo3temp/var/tests/acceptance-composer',
+    ];
+
+    public static $events = [
+        Events::SUITE_BEFORE => 'bootstrapTypo3Environment',
+    ];
+
+    public function bootstrapTypo3Environment()
+    {
+        // @todo: ugly workaround for InstallTool/AbstractCest.php
+        $root = realpath(__DIR__ . '/../../../../../../../' . $this->config['typo3InstancePath']);
+        chdir($root);
+        putenv('TYPO3_ACCEPTANCE_PATH_WEB=' . $root . '/public');
+        putenv('TYPO3_ACCEPTANCE_PATH_VAR=' . $root . '/var');
+        putenv('TYPO3_ACCEPTANCE_PATH_CONFIG=' . $root . '/config');
+        putenv('TYPO3_ACCEPTANCE_INSTALLTOOL_PW_PRESET=1');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
index dfd3c3330998..df19aa525cbe 100644
--- a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
+++ b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
@@ -152,6 +152,10 @@ final class ApplicationEnvironment extends BackendEnvironment
                 );
             }
         }
+        // @todo: ugly workaround for InstallTool/AbstractCest.php
+        putenv('TYPO3_ACCEPTANCE_PATH_WEB=' . $instancePath);
+        putenv('TYPO3_ACCEPTANCE_PATH_VAR=' . $instancePath . '/typo3temp/var');
+        putenv('TYPO3_ACCEPTANCE_PATH_CONFIG=' . $instancePath . '/typo3conf');
     }
 
     // @todo Eventually move this up to TF::BackendEnvironment, but then as protected.
-- 
GitLab