From 11102a5f441f9159642f5bacbe2823face81792d Mon Sep 17 00:00:00 2001 From: Benjamin Franzke Date: Sat, 17 Feb 2024 10:33:23 +0100 Subject: [PATCH] [TASK] Add composer-mode to our acceptance test matrix 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. The v12 backport additionally imports pieces from #102440 as t3editor extension is enabled in this composer-mode, while v12 classic mode is tested (for historic reasons) without t3editor extension enabled. Commands executed: > composer require --dev "typo3/cms-styleguide":"^12.0.5" Resolves: #103297 Releases: main, 12.4, 11.5 Change-Id: I64973f110931b51ed2ef7ef8f8cc3411834fcf37 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83263 Reviewed-by: Benjamin Franzke Reviewed-by: Christian Kuhn Tested-by: Benjamin Franzke Tested-by: core-ci Tested-by: Christian Kuhn --- Build/Scripts/runTests.sh | 82 +++++++- Build/Scripts/setupAcceptanceComposer.sh | 51 +++++ .../t3editor/element/code-mirror-element.ts | 22 +++ Build/composer/composer.dist.json | 6 +- Build/gitlab-ci.yml | 2 + .../acceptance-application-composer.yml | 180 ++++++++++++++++++ .../acceptance-application-composer.yml | 17 ++ .../Classes/Command/DatasetImportCommand.php | 50 +++++ .../Configuration/Services.yaml | 8 + .../packages/dataset_import/composer.json | 20 ++ composer.json | 2 +- composer.lock | 14 +- .../Tests/Acceptance/Application.suite.yml | 6 +- .../Application/BackendUser/ListUserCest.php | 100 +++++++--- .../Extensionmanager/GetExtensionsCest.php | 5 + .../InstalledExtensionsCest.php | 3 + .../FileList/FileOperationsCest.php | 24 ++- .../FormEngine/FalMetadataCest.php | 10 +- .../Application/FormEngine/Inline1nCest.php | 10 + .../Frontend/ContentElementsCest.php | 14 +- .../Frontend/FormFrameworkCest.php | 14 +- .../Frontend/FrontendLoginCest.php | 14 +- .../Frontend/IndexedSearchCest.php | 14 +- .../Application/Impexp/ExportCest.php | 14 +- .../Application/Impexp/ImportCest.php | 9 +- .../Application/Impexp/UsersCest.php | 30 +-- .../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 | 40 ++-- .../ApplicationComposerEnvironment.php | 49 +++++ .../Extension/ApplicationEnvironment.php | 4 + .../Acceptance/Support/Helper/PageTree.php | 78 ++++++++ .../Classes/Form/Element/T3editorElement.php | 1 + .../JavaScript/element/code-mirror-element.js | 2 +- 38 files changed, 814 insertions(+), 118 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 330b269a2d20..39fde4378daf 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -177,6 +177,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 @@ -239,7 +240,7 @@ Options: - pdo_mysql -d - 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 @@ -298,12 +299,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 @@ -571,9 +572,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} @@ -635,6 +636,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 @@ -1014,7 +1084,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..8f220c1d1ae4 --- /dev/null +++ b/Build/Scripts/setupAcceptanceComposer.sh @@ -0,0 +1,51 @@ +#!/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 +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..db8409ab5d24 --- /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": "12.4.*@dev", + "typo3/testing-framework": "^8.0.9" + }, + "extra": { + "typo3/cms": { + "extension-key": "dataset_import" + } + }, + "autoload": { + "psr-4": { + "TYPO3Tests\\DatasetImport\\": "Classes/" + } + } +} diff --git a/composer.json b/composer.json index 426a5aaa3497..f3e1a1f64ccf 100644 --- a/composer.json +++ b/composer.json @@ -118,7 +118,7 @@ "phpunit/phpunit": "^10.5.5", "sokil/php-isocodes-db-i18n": "^4.0.20", "symfony/translation": "^6.4 || ^7.0", - "typo3/cms-styleguide": "^12.0.3", + "typo3/cms-styleguide": "^12.0.5", "typo3/testing-framework": "^8.0.9", "webmozart/assert": "^1.11.0" }, diff --git a/composer.lock b/composer.lock index af08eef1f96b..71763509915d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3c021a2995c354f02c332ef952c6b246", + "content-hash": "052ed9f31f9b960e4d5dff92d9f1e01c", "packages": [ { "name": "bacon/bacon-qr-code", @@ -8930,16 +8930,16 @@ }, { "name": "typo3/cms-styleguide", - "version": "12.0.3", + "version": "12.0.5", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/styleguide.git", - "reference": "170f06dff0c7af136cb0912c15d77795da4bcedc" + "reference": "dfb2cd72d8b78dbb7ed7ca7f27dedd1352bc377a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/styleguide/zipball/170f06dff0c7af136cb0912c15d77795da4bcedc", - "reference": "170f06dff0c7af136cb0912c15d77795da4bcedc", + "url": "https://api.github.com/repos/TYPO3-CMS/styleguide/zipball/dfb2cd72d8b78dbb7ed7ca7f27dedd1352bc377a", + "reference": "dfb2cd72d8b78dbb7ed7ca7f27dedd1352bc377a", "shasum": "" }, "require-dev": { @@ -8995,9 +8995,9 @@ ], "support": { "issues": "https://github.com/TYPO3/styleguide/issues", - "source": "https://github.com/TYPO3-CMS/styleguide/tree/12.0.3" + "source": "https://github.com/TYPO3-CMS/styleguide/tree/12.0.5" }, - "time": "2024-01-06T11:20:20+00:00" + "time": "2024-03-13T07:40:32+00:00" }, { "name": "typo3/testing-framework", 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 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Application\BackendUser; +use Codeception\Scenario; use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester; /** @@ -34,23 +35,38 @@ public function _before(ApplicationTester $I): void $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 @@ public function filterUsersByUsername(ApplicationTester $I): void $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 @@ public function filterUsersByAdmin(ApplicationTester $I): void $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 @@ public function filterUsersByAdmin(ApplicationTester $I): void $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 @@ public function filterUsersByStatus(ApplicationTester $I): void $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 @@ public function filterUsersByStatus(ApplicationTester $I): void $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 @@ public function filterUsersByLogin(ApplicationTester $I): void $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 @@ public function filterUsersByUserGroup(ApplicationTester $I): void 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 @@ 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 @@ public function _before(ApplicationTester $I): void $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 @@ public function checkSearchFilterListFindsExtensionKey(ApplicationTester $I): vo $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 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Extensionmanager; +use Codeception\Attribute\Env; use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester; /** @@ -61,6 +62,7 @@ public function checkSearchFiltersList(ApplicationTester $I): void $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 @@ public function checkIfUploadFormAppears(ApplicationTester $I): void $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/FileList/FileOperationsCest.php b/typo3/sysext/core/Tests/Acceptance/Application/FileList/FileOperationsCest.php index ebf4bf0b7bc7..90c45509995f 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/FileList/FileOperationsCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/FileList/FileOperationsCest.php @@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Application\FileList; +use Codeception\Scenario; use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester; use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\FileTree; use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\ModalDialog; @@ -33,9 +34,12 @@ public function _before(ApplicationTester $I, FileTree $tree): void $I->switchToContentFrame(); } - public function fileCrud(ApplicationTester $I, ModalDialog $modalDialog): void + public function fileCrud(ApplicationTester $I, ModalDialog $modalDialog, Scenario $scenario): void { $fileTextareaSelector = 'textarea[name="data[editfile][0][data]"]'; + $codeMirrorSelector = 'typo3-t3editor-codemirror[name="data[editfile][0][data]"]'; + $isComposerMode = str_contains($scenario->current('env'), 'composer'); + $fileName = 'typo3-test.txt'; $flashMessageSelector = '.typo3-messages'; @@ -48,13 +52,23 @@ public function fileCrud(ApplicationTester $I, ModalDialog $modalDialog): void $I->wait(0.2); $I->click('Create file'); $I->see('File created:', $flashMessageSelector); - $I->fillField($fileTextareaSelector, 'Some Text'); + if ($isComposerMode) { + $I->waitForElementVisible($codeMirrorSelector); + $I->executeJS("document.querySelector('" . $codeMirrorSelector . "').setContent('Some Text')"); + } else { + $I->fillField($fileTextareaSelector, 'Some Text'); + } // Save file $I->amGoingTo('save the file'); $I->click('.module-docheader button[name="_save"]'); - $textareaValue = $I->grabValueFrom($fileTextareaSelector); - $I->assertEquals('Some Text', $textareaValue); + if ($isComposerMode) { + $I->waitForElementVisible($codeMirrorSelector); + $I->executeJS("console.assert(document.querySelector('" . $codeMirrorSelector . "').getContent() === 'Some Text')"); + } else { + $textareaValue = $I->grabValueFrom($fileTextareaSelector); + $I->assertEquals('Some Text', $textareaValue); + } $I->see('File saved to', $flashMessageSelector); // Save file @@ -65,7 +79,7 @@ public function fileCrud(ApplicationTester $I, ModalDialog $modalDialog): void // Delete file $I->amGoingTo('delete the file'); $I->clickWithRightButton('[data-filelist-identifier="1:/' . $fileName . '"] [data-filelist-action="primary"]'); - $I->click('[data-title="Delete"]'); + $I->click('.context-menu [data-title="Delete"]'); $modalDialog->canSeeDialog(); $modalDialog->clickButtonInDialog('Yes, delete this file'); $I->waitForElementNotVisible('[data-filelist-identifier="1:/' . $fileName . '"]'); diff --git a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/FalMetadataCest.php b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/FalMetadataCest.php index f585ab629278..265e262b4dd1 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 @@ public function checkIfUpdatedFileMetadataIsUpdatedInContent(ApplicationTester $ $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 @@ public function checkIfDeactivatingNullCheckboxesFocusesTextFields(ApplicationTe $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 a24dd105a1d6..88d5d0b2fcaa 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/Inline1nCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/Inline1nCest.php @@ -90,6 +90,11 @@ public function createInline1nInlineElement(ApplicationTester $I): void $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); } /** @@ -112,6 +117,11 @@ public function checkIfCanSortingInlineElement(ApplicationTester $I): void $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 830798e0c8cd..dd7cc8ebcda2 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/ContentElementsCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/ContentElementsCest.php @@ -32,6 +32,10 @@ public function _before(ApplicationTester $I, PageTree $pageTree): void $I->waitForElement('#typo3-pagetree-tree .nodes .node', 5); $pageTree->openPath(['styleguide frontend demo']); $I->switchToContentFrame(); + $I->wait(1); + $I->waitForElementVisible('select[name=actionMenu]'); + $I->selectOption('select[name=actionMenu]', 'Layout'); + $I->wait(1); $I->waitForElementVisible('.t3js-module-docheader-bar a[title="View webpage"]'); $I->wait(1); $I->click('.t3js-module-docheader-bar a[title="View webpage"]'); @@ -50,9 +54,13 @@ public function _after(ApplicationTester $I): void // 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 4adbebec7264..47dffff5d7e1 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FormFrameworkCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FormFrameworkCest.php @@ -38,6 +38,10 @@ public function _before(ApplicationTester $I, PageTree $pageTree): void $I->waitForElement('#typo3-pagetree-tree .nodes .node', 5); $pageTree->openPath(['styleguide frontend demo']); $I->switchToContentFrame(); + $I->wait(1); + $I->waitForElementVisible('select[name=actionMenu]'); + $I->selectOption('select[name=actionMenu]', 'Layout'); + $I->wait(1); $I->waitForElementVisible('.t3js-module-docheader-bar a[title="View webpage"]'); $I->wait(1); $I->click('.t3js-module-docheader-bar a[title="View webpage"]'); @@ -58,9 +62,13 @@ public function _after(ApplicationTester $I): void // 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 5eafbde7fbd7..ffd53fb9165a 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FrontendLoginCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/FrontendLoginCest.php @@ -36,6 +36,10 @@ public function _before(ApplicationTester $I, PageTree $pageTree): void $I->waitForElement('#typo3-pagetree-tree .nodes .node', 5); $pageTree->openPath(['styleguide frontend demo']); $I->switchToContentFrame(); + $I->wait(1); + $I->waitForElementVisible('select[name=actionMenu]'); + $I->selectOption('select[name=actionMenu]', 'Layout'); + $I->wait(1); $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 +60,13 @@ public function _after(ApplicationTester $I): void // 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 4f43563b9211..72c1857e4312 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Frontend/IndexedSearchCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Frontend/IndexedSearchCest.php @@ -37,6 +37,10 @@ public function _before(ApplicationTester $I, PageTree $pageTree): void $I->waitForElement('#typo3-pagetree-tree .nodes .node', 5); $pageTree->openPath(['styleguide frontend demo']); $I->switchToContentFrame(); + $I->wait(1); + $I->waitForElementVisible('select[name=actionMenu]'); + $I->selectOption('select[name=actionMenu]', 'Layout'); + $I->wait(1); $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 +61,13 @@ public function _after(ApplicationTester $I): void // 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/ExportCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php index 18d54842e20c..ad682b65f56f 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php @@ -28,7 +28,6 @@ final class ExportCest extends AbstractCest { private string $contextMenuExport = '[data-callback-action=exportT3d]'; private string $contextMenuMore = 'li.context-menu-item-submenu'; - private string $inPageTree = '#typo3-pagetree-treeContainer .nodes'; private string $inModuleHeader = '.module-docheader'; private string $inModuleTabs = '#ImportExportController .nav-tabs'; private string $inModuleTabsBody = '#ImportExportController .tab-content'; @@ -45,7 +44,6 @@ public function _before(ApplicationTester $I): void public function exportPageAndRecordsDisplaysTitleOfSelectedPageInModuleHeader(ApplicationTester $I, PageTree $pageTree): void { $pageTree->openPath(['styleguide TCA demo']); - $I->waitForElement($this->inPageTree . ' .node', 5); $selectedPageTitle = 'elements t3editor'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; @@ -70,10 +68,7 @@ public function exportTableDisplaysTitleOfRootPageInModuleHeader(ApplicationTest $listModuleHeader = '.module-docheader'; $listModuleBtnExport = 'a[title="Export"]'; - $pageTree->openPath(['styleguide TCA demo']); - $I->waitForElement($this->inPageTree . ' .node', 5); - - $pageTree->openPath([$tablePageTitle]); + $pageTree->openPath(['styleguide TCA demo', $tablePageTitle]); $I->switchToContentFrame(); // List module single table mode $I->waitForText($tableTitle); @@ -99,10 +94,7 @@ public function exportRecordDisplaysTitleOfRootPageInModuleHeader(ApplicationTes $recordTable = '#recordlist-tx_styleguide_elements_t3editor'; $recordIcon = 'tr:first-child button[data-contextmenu-trigger]'; - $pageTree->openPath(['styleguide TCA demo']); - $I->waitForElement($this->inPageTree . ' .node', 5); - - $pageTree->openPath([$recordPageTitle]); + $pageTree->openPath(['styleguide TCA demo', $recordPageTitle]); $I->switchToContentFrame(); // List module single table mode $I->waitForText($recordPageTitle); @@ -133,7 +125,6 @@ public function saveAndDeletePresetSucceeds(ApplicationTester $I, ModalDialog $m $selectPreset = 'select[name="preset[select]"]'; $pageTree->openPath(['styleguide TCA demo']); - $I->waitForElement($this->inPageTree . ' .node', 5); $I->click($pageIcon); $this->selectInContextMenu($I, [$this->contextMenuMore, $this->contextMenuExport]); @@ -183,7 +174,6 @@ public function exportPageAndRecordsFromPageTree(ApplicationTester $I, PageTree $buttonSaveToFile = 'tx_impexp[save_export]'; $pageTree->openPath(['styleguide TCA demo']); - $I->waitForElement($this->inPageTree . ' .node', 5); $I->click($pageIcon); $this->selectInContextMenu($I, [$this->contextMenuMore, $this->contextMenuExport]); diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ImportCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ImportCest.php index 3daaf18ed928..d2c6065c5cc2 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 @@ 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; @@ -146,7 +145,7 @@ public function rejectImportIfPrerequisitesNotMet(ApplicationTester $I, ModalDia $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; @@ -183,7 +182,7 @@ public function importPageAndRecords(ApplicationTester $I, ModalDialog $modalDia $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; @@ -224,7 +223,7 @@ public function importTable(ApplicationTester $I, ModalDialog $modalDialog, Page $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; @@ -267,7 +266,7 @@ public function importRecord(ApplicationTester $I, ModalDialog $modalDialog, Pag $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/Impexp/UsersCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php index aa9d22db5ad9..70c34d5256c4 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php @@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Impexp; +use Codeception\Scenario; use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester; use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\PageTree; @@ -43,14 +44,14 @@ public function _before(ApplicationTester $I): void $I->waitForElement('svg .nodes .node'); } - public function doNotShowImportAndExportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void + public function doNotShowImportAndExportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree, Scenario $scenario): void { $selectedPageTitle = 'Root'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; $this->setPageAccess($I, $pageTree, [$selectedPageTitle], 1); $this->setModAccess($I, 1, ['web_list' => true]); - $this->setUserTsConfig($I, 2, ''); + $this->setUserTsConfig($I, $scenario, 2, ''); $I->useExistingSession('editor'); $I->click($selectedPageIcon); @@ -62,12 +63,12 @@ public function doNotShowImportAndExportInContextMenuForNonAdminUser(Application $I->useExistingSession('admin'); } - public function showImportExportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void + public function showImportExportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I, Scenario $scenario): void { $selectedPageTitle = 'Root'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; - $this->setUserTsConfig($I, 2, "options.impexp.enableImportForNonAdminUser = 1\noptions.impexp.enableExportForNonAdminUser = 1"); + $this->setUserTsConfig($I, $scenario, 2, "options.impexp.enableImportForNonAdminUser = 1\noptions.impexp.enableExportForNonAdminUser = 1"); $I->useExistingSession('editor'); $I->click($selectedPageIcon); @@ -101,7 +102,7 @@ public function hideImportCheckboxForceAllUidsForNonAdmin(ApplicationTester $I): $I->useExistingSession('admin'); } - public function hideUploadTabAndImportPathIfNoImportFolderAvailable(ApplicationTester $I, PageTree $pageTree): void + public function hideUploadTabAndImportPathIfNoImportFolderAvailable(ApplicationTester $I, PageTree $pageTree, Scenario $scenario): void { $selectedPageTitle = 'Root'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; @@ -126,10 +127,10 @@ public function hideUploadTabAndImportPathIfNoImportFolderAvailable(ApplicationT $this->setPageAccess($I, $pageTree, ['Root'], 0); $this->setModAccess($I, 1, ['web_list' => false]); - $this->setUserTsConfig($I, 2, ''); + $this->setUserTsConfig($I, $scenario, 2, ''); } - public function checkVisualElements(ApplicationTester $I, PageTree $pageTree): void + public function checkVisualElements(ApplicationTester $I, PageTree $pageTree, Scenario $scenario): void { $selectedPageTitle = 'Root'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; @@ -151,7 +152,7 @@ public function checkVisualElements(ApplicationTester $I, PageTree $pageTree): v $this->setPageAccess($I, $pageTree, ['Root'], 1); $this->setModAccess($I, 1, ['web_list' => true]); - $this->setUserTsConfig($I, 2, 'options.impexp.enableImportForNonAdminUser = 1'); + $this->setUserTsConfig($I, $scenario, 2, 'options.impexp.enableImportForNonAdminUser = 1'); $I->useExistingSession('editor'); $I->click($selectedPageIcon); @@ -163,7 +164,7 @@ public function checkVisualElements(ApplicationTester $I, PageTree $pageTree): v $this->setPageAccess($I, $pageTree, ['Root'], 0); $this->setModAccess($I, 1, ['web_list' => false]); - $this->setUserTsConfig($I, 2, ''); + $this->setUserTsConfig($I, $scenario, 2, ''); } private function setPageAccess(ApplicationTester $I, PageTree $pageTree, array $pagePath, int $userGroupId, int $recursionLevel = 1): void @@ -211,7 +212,7 @@ private function setModAccess(ApplicationTester $I, int $userGroupId, array $mod $I->waitForText('Backend user groups'); } - private function setUserTsConfig(ApplicationTester $I, int $userId, string $userTsConfig): void + private function setUserTsConfig(ApplicationTester $I, Scenario $scenario, int $userId, string $userTsConfig): void { try { $I->seeElement($this->inModuleHeader . ' [name=BackendUserModuleMenu]'); @@ -227,7 +228,14 @@ private function setUserTsConfig(ApplicationTester $I, int $userId, string $user $I->click('//table[@id="typo3-backend-user-list"]/tbody/tr[descendant::button[@data-contextmenu-uid="' . $userId . '"]]//a[@title="Edit"]'); $I->waitForElement('#EditDocumentController'); $I->click('//form[@id="EditDocumentController"]//ul/li[5]/a'); - $I->fillField('//div[@class="tab-content"]/div[5]/fieldset[1]//textarea', $userTsConfig); + $isComposerMode = str_contains($scenario->current('env'), 'composer'); + if ($isComposerMode) { + $codeMirrorSelector = 'typo3-t3editor-codemirror[name="data[be_users][' . $userId . '][TSconfig]"]'; + $I->waitForElementVisible($codeMirrorSelector); + $I->executeJS("document.querySelector('" . $codeMirrorSelector . "').setContent('" . implode('\n', explode("\n", $userTsConfig)) . "')"); + } else { + $I->fillField('//div[@class="tab-content"]/div[5]/fieldset[1]//textarea', $userTsConfig); + } $I->click($this->inModuleHeader . ' .btn[title="Save"]'); $I->wait(0.5); $I->click($this->inModuleHeader . ' .btn[title="Close"]'); 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 @@ 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 @@ 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 @@ public function _after(ApplicationTester $I): void $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 @@ protected function logIntoInstallTool(ApplicationTester $I): void 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, '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 38209edb8818..64082890faa4 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 @@ 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 @@ public function seeConfigurationPresets(ApplicationTester $I, ModalDialog $modal $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 @@ public function seeFeatureToggles(ApplicationTester $I, ModalDialog $modalDialog // 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 2f111e5d828f..9aece6cc2cb0 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 @@ 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; @@ -32,6 +33,7 @@ public function _before(ApplicationTester $I): void $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 b2c604c3db1d..f2ca2d34da74 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 @@ public function canEditRecordFromListView(ApplicationTester $I): void $I->canSee('Redirect Management', 'h1'); $I->amGoingTo('test edit on edit button'); - $I->click('table.table-striped > tbody > tr > td:nth-child(7) > 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 @@ public function canSwitchToInformation(ApplicationTester $I): void $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 f728aa904311..5517ced18d52 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php @@ -17,7 +17,9 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Template; +use Codeception\Scenario; use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester; +use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\PageTree; final class TemplateCest { @@ -57,12 +59,11 @@ public function pagesWithNoTemplateShouldShowButtonsToCreateTemplates(Applicatio $I->see('You need to create a TypoScript record in order to edit your configuration.'); } - public function addANewSiteTemplate(ApplicationTester $I): void + public function addANewSiteTemplate(ApplicationTester $I, PageTree $pageTree, Scenario $scenario): void { - $I->waitForText('TypoScript records'); $I->wantTo('create a new root TypoScript record'); $I->switchToMainFrame(); - $I->clickWithLeftButton('//*[text()=\'styleguide TCA demo\']'); + $pageTree->openPath(['styleguide TCA demo']); $I->switchToContentFrame(); $I->waitForElementVisible('.t3-js-jumpMenuBox'); $I->waitForElementNotVisible('#nprogress', 120); @@ -94,10 +95,19 @@ public function addANewSiteTemplate(ApplicationTester $I): void $I->waitForElementNotVisible('#t3js-ui-block'); $I->wantTo('change the setup, save the TypoScript record and close the form'); - // grab and fill setup textarea - $config = $I->grabTextFrom('//textarea[contains(@data-formengine-input-name, "data[sys_template]") and contains(@data-formengine-input-name, "[config]")]'); - $config = str_replace('HELLO WORLD!', 'Hello Acceptance Test!', $config); - $I->fillField('//textarea[contains(@data-formengine-input-name, "data[sys_template]") and contains(@data-formengine-input-name, "[config]")]', $config); + + $isComposerMode = str_contains($scenario->current('env'), 'composer'); + if ($isComposerMode) { + $codeMirrorSelector = 'typo3-t3editor-codemirror[name$="[config]"]'; + $I->waitForElementVisible($codeMirrorSelector); + $I->executeJS("const codeMirror = document.querySelector('" . $codeMirrorSelector . "'); const config = codeMirror.getContent().replace('HELLO WORLD!', 'Hello Acceptance Test!'); codeMirror.setContent(config)"); + } else { + // grab and fill setup textarea + $config = $I->grabTextFrom('//textarea[contains(@data-formengine-input-name, "data[sys_template]") and contains(@data-formengine-input-name, "[config]")]'); + $config = str_replace('HELLO WORLD!', 'Hello Acceptance Test!', $config); + $I->fillField('//textarea[contains(@data-formengine-input-name, "data[sys_template]") and contains(@data-formengine-input-name, "[config]")]', $config); + } + $I->switchToMainFrame(); $I->waitForElementNotVisible('typo3-notification-message', 20); $I->switchToContentFrame(); @@ -134,13 +144,11 @@ public function addANewSiteTemplate(ApplicationTester $I): void $I->see('value = HELLO WORLD!'); } - public function checkClosestTemplateButton(ApplicationTester $I): void + public function checkClosestTemplateButton(ApplicationTester $I, PageTree $pageTree): void { $I->wantTo('click on the button to go to the closest page with a TypoScript record'); $I->switchToMainFrame(); - // Open the pagetree - $I->clickWithLeftButton('(//*[contains(concat(" ", normalize-space(@class), " "), " node-toggle ")])[4]'); - $I->clickWithLeftButton('//*[text()=\'menu_sitemap_pages\']'); + $pageTree->openPath(['styleguide frontend demo', 'menu_sitemap_pages']); $I->switchToContentFrame(); $I->waitForElementVisible('.t3-js-jumpMenuBox'); $I->waitForElementNotVisible('#nprogress', 120); @@ -167,15 +175,17 @@ public function checkClosestTemplateButton(ApplicationTester $I): void $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): void + public function createExtensionTemplate(ApplicationTester $I, PageTree $pageTree): void { $I->wantTo('see the button to create an additional TypoScript record'); $I->switchToMainFrame(); - //Open the pagetree - $I->clickWithLeftButton('(//*[contains(concat(" ", normalize-space(@class), " "), " node-toggle ")])[4]'); - $I->clickWithLeftButton('//*[text()=\'menu_sitemap_pages\']'); + $pageTree->openPath(['styleguide frontend demo', 'menu_sitemap_pages']); $I->switchToContentFrame(); $I->waitForElementVisible('.t3-js-jumpMenuBox'); $I->waitForElementNotVisible('#nprogress', 120); 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 @@ + '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 56526f4b208e..9bad0f83ad91 100644 --- a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php +++ b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php @@ -157,6 +157,10 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent): void ); } } + // @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. diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Helper/PageTree.php b/typo3/sysext/core/Tests/Acceptance/Support/Helper/PageTree.php index 057afbee5236..411ab4fe5fce 100644 --- a/typo3/sysext/core/Tests/Acceptance/Support/Helper/PageTree.php +++ b/typo3/sysext/core/Tests/Acceptance/Support/Helper/PageTree.php @@ -18,6 +18,7 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Support\Helper; use Facebook\WebDriver\Remote\RemoteWebDriver; +use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverKeys; use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester; @@ -36,6 +37,83 @@ public function __construct(ApplicationTester $I, Mouse $mouse) $this->mouse = $mouse; } + /** + * Waits until tree nodes are rendered + */ + public function waitForNodes(): void + { + $this->tester->waitForElement(static::$pageTreeSelector . ' ' . static::$treeItemSelector, 5); + } + + /** + * Open the given hierarchical path in the pagetree and click the last page. + * + * Example to open "styleguide -> elements basic" page: + * [ + * 'styleguide TCA demo', + * 'elements basic', + * ] + * + * @param string[] $path + */ + public function openPath(array $path) + { + $context = $this->getPageTreeElement(); + + $this->waitForNodes(); + + // Collapse all opened paths (might be opened due to localstorage) + do { + $toggled = false; + try { + // collapse last opened node element, that is not the root (=first node) + $context->findElement(\Facebook\WebDriver\WebDriverBy::xpath('(.//*[position()>1 and @role="treeitem" and */@class="node-toggle node-toggle--expanded chevron expanded"])[last()]/*[starts-with(@class, "node-toggle")]'))->click(); + $toggled = true; + } catch (\Facebook\WebDriver\Exception\NoSuchElementException $e) { + // element not found so it may be already opened... + } catch (\Facebook\WebDriver\Exception\ElementNotVisibleException $e) { + // element not found so it may be already opened... + } catch (\Facebook\WebDriver\Exception\ElementNotInteractableException $e) { + // another possible exception if the chevron isn't there ... depends on facebook driver version + } + } while ($toggled); + + parent::openPath($path); + } + + /** + * Search for an element with the given link text in the provided context. + * + * @param string $nodeText + * @param RemoteWebElement $context + * @return RemoteWebElement + */ + protected function ensureTreeNodeIsOpen(string $nodeText, RemoteWebElement $context) + { + $I = $this->tester; + $I->see($nodeText, self::$treeItemSelector); + + /** @var RemoteWebElement $context */ + $context = $I->executeInSelenium(function () use ( + $nodeText, + $context + ) { + return $context->findElement(\Facebook\WebDriver\WebDriverBy::xpath('//*[text()=\'' . $nodeText . '\']/..')); + }); + + try { + $context->findElement(\Facebook\WebDriver\WebDriverBy::cssSelector('.chevron.collapsed[visibility="visible"]'))->click(); + } catch (\Facebook\WebDriver\Exception\NoSuchElementException $e) { + // element not found so it may be already opened... + } catch (\Facebook\WebDriver\Exception\ElementNotVisibleException $e) { + // element not found so it may be already opened... + } catch (\Facebook\WebDriver\Exception\ElementNotInteractableException $e) { + // another possible exception if the chevron isn't there ... depends on facebook driver version + } + + return $context; + } + /** * Perform drag and drop for a new page into the given target page. */ diff --git a/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php b/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php index 45a13a55ae6f..9d28e6212fc9 100644 --- a/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php +++ b/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php @@ -214,6 +214,7 @@ protected function getHTMLCodeForEditor( } } $codeMirrorConfig = [ + 'name' => $name, 'mode' => GeneralUtility::jsonEncodeForHtmlAttribute($mode->getModule(), false), 'addons' => GeneralUtility::jsonEncodeForHtmlAttribute($addons, false), 'keymaps' => GeneralUtility::jsonEncodeForHtmlAttribute($keymaps, false), diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js index 3e85a8648451..c15b16b82d81 100644 --- a/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js +++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js @@ -15,7 +15,7 @@ var __decorate=function(e,t,o,r){var i,a=arguments.length,l=a<3?t:null===r?r=Obj
this.onKeydown(e)}>
${this.label&&"bottom"===this.panel?html`
${this.label}
`:""} ${null===this.editorView?html``:""} - `}firstUpdated(){if(this.nolazyload)return void this.initializeEditor(this.firstElementChild);const e={root:document.body},t=new IntersectionObserver((e=>{e.forEach((e=>{e.intersectionRatio>0&&(t.unobserve(e.target),this.firstElementChild&&"textarea"===this.firstElementChild.nodeName.toLowerCase()&&this.initializeEditor(this.firstElementChild))}))}),e);t.observe(this)}onKeydown(e){e.ctrlKey&&e.altKey&&"f"===e.key&&(e.preventDefault(),this.fullscreen=!0),"Escape"===e.key&&this.fullscreen&&(e.preventDefault(),this.fullscreen=!1)}async initializeEditor(e){const t=EditorView.updateListener.of((t=>{t.docChanged&&(e.value=t.state.doc.toString(),e.dispatchEvent(new CustomEvent("change",{bubbles:!0})))}));this.lineDigits>0?this.style.setProperty("--rows",this.lineDigits.toString()):e.getAttribute("rows")&&this.style.setProperty("--rows",e.getAttribute("rows")),this.editorTheme=new Compartment;const o=[this.editorTheme.of([]),t,lineNumbers(),highlightSpecialChars(),drawSelection(),EditorState.allowMultipleSelections.of(!0),syntaxHighlighting(defaultHighlightStyle,{fallback:!0})];if(this.readonly&&o.push(EditorState.readOnly.of(!0)),this.placeholder&&o.push(placeholder(this.placeholder)),this.mode){const e=await executeJavaScriptModuleInstruction(this.mode);o.push(...e)}this.addons.length>0&&o.push(...await Promise.all(this.addons.map((e=>executeJavaScriptModuleInstruction(e)))));const r=[...defaultKeymap,indentWithTab];if(this.keymaps.length>0){const e=await Promise.all(this.keymaps.map((e=>loadModule(e).then((t=>resolveSubjectRef(t,e))))));e.forEach((e=>r.push(...e)))}o.push(keymap.of(r)),this.editorView=new EditorView({state:EditorState.create({doc:e.value,extensions:o}),parent:this.renderRoot.querySelector("#codemirror-parent"),root:this.renderRoot});const i=window.matchMedia("(prefers-color-scheme: dark)");this.toggleDarkMode(i.matches),i.addEventListener("change",(e=>{this.toggleDarkMode(e.matches)}))}toggleDarkMode(e){this.editorView.dispatch({effects:this.editorTheme.reconfigure(e?oneDark:[])})}};CodeMirrorElement.styles=css` + `}firstUpdated(){if(this.nolazyload)return void this.initializeEditor(this.firstElementChild);const e={root:document.body},t=new IntersectionObserver((e=>{e.forEach((e=>{e.intersectionRatio>0&&(t.unobserve(e.target),this.firstElementChild&&"textarea"===this.firstElementChild.nodeName.toLowerCase()&&this.initializeEditor(this.firstElementChild))}))}),e);t.observe(this)}setContent(e){null!==this.editorView&&this.editorView.dispatch({changes:{from:0,to:this.editorView.state.doc.length,insert:e}})}getContent(){return this.editorView.state.doc.toString()}onKeydown(e){e.ctrlKey&&e.altKey&&"f"===e.key&&(e.preventDefault(),this.fullscreen=!0),"Escape"===e.key&&this.fullscreen&&(e.preventDefault(),this.fullscreen=!1)}async initializeEditor(e){const t=EditorView.updateListener.of((t=>{t.docChanged&&(e.value=t.state.doc.toString(),e.dispatchEvent(new CustomEvent("change",{bubbles:!0})))}));this.lineDigits>0?this.style.setProperty("--rows",this.lineDigits.toString()):e.getAttribute("rows")&&this.style.setProperty("--rows",e.getAttribute("rows")),this.editorTheme=new Compartment;const o=[this.editorTheme.of([]),t,lineNumbers(),highlightSpecialChars(),drawSelection(),EditorState.allowMultipleSelections.of(!0),syntaxHighlighting(defaultHighlightStyle,{fallback:!0})];if(this.readonly&&o.push(EditorState.readOnly.of(!0)),this.placeholder&&o.push(placeholder(this.placeholder)),this.mode){const e=await executeJavaScriptModuleInstruction(this.mode);o.push(...e)}this.addons.length>0&&o.push(...await Promise.all(this.addons.map((e=>executeJavaScriptModuleInstruction(e)))));const r=[...defaultKeymap,indentWithTab];if(this.keymaps.length>0){const e=await Promise.all(this.keymaps.map((e=>loadModule(e).then((t=>resolveSubjectRef(t,e))))));e.forEach((e=>r.push(...e)))}o.push(keymap.of(r)),this.editorView=new EditorView({state:EditorState.create({doc:e.value,extensions:o}),parent:this.renderRoot.querySelector("#codemirror-parent"),root:this.renderRoot});const i=window.matchMedia("(prefers-color-scheme: dark)");this.toggleDarkMode(i.matches),i.addEventListener("change",(e=>{this.toggleDarkMode(e.matches)}))}toggleDarkMode(e){this.editorView.dispatch({effects:this.editorTheme.reconfigure(e?oneDark:[])})}};CodeMirrorElement.styles=css` @media (prefers-color-scheme: dark) { :host { color-scheme: dark;