diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..452a0d9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:api.github.com)" + ] + } +} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..46a3e42 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,61 @@ +name: E2E Tests + +on: + pull_request: + branches: [ trunk, develop ] + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install Composer dependencies + run: composer install --no-dev --no-progress --prefer-dist + + - name: Install npm dependencies + run: npm ci --force + + - name: Build plugin assets + run: npm run build + + - name: Install Playwright Chromium + run: npx playwright install chromium --with-deps + + - name: Start wp-env + run: npx wp-env start + + - name: Run E2E tests + env: + WP_BASE_URL: http://localhost:8888 + WP_USERNAME: admin + WP_PASSWORD: password + run: npm run test:e2e + + - name: Stop wp-env + if: always() + run: npx wp-env stop + + - name: Upload test artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results + path: | + test-results/ + playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 96067a5..e9d431e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ build/* !build/index.php +# Playwright +test-results/ +playwright-report/ +tests/e2e/artifacts/ +tests/e2e/.env + # Created by https://www.toptal.com/developers/gitignore/api/macos,linux,windows,node,yarn,composer,phpstorm+all,visualstudiocode,sublimetext # Edit at https://www.toptal.com/developers/gitignore?templates=macos,linux,windows,node,yarn,composer,phpstorm+all,visualstudiocode,sublimetext diff --git a/.wp-env.json b/.wp-env.json index 01218a3..35df874 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,6 +1,6 @@ { "core": null, - "phpVersion": "8.2", + "phpVersion": "8.3", "plugins": [ "." ], "config": { "WP_DEBUG": true, diff --git a/composer.json b/composer.json index f2f3026..4d730cf 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,9 @@ }, "require-dev": { "a8cteam51/team51-configs": "dev-trunk", - "wp-coding-standards/wpcs": "3.1.*", "phpcompatibility/phpcompatibility-wp": "*", - "roave/security-advisories": "dev-latest", - "phpunit/phpunit": "^9.6", "yoast/phpunit-polyfills": "^2.0" }, @@ -38,7 +35,6 @@ "format:php": "phpcbf --basepath=. . -v", "lint:php": "phpcs --basepath=. . -v", - "packages-install": "@composer install --ignore-platform-reqs --no-interaction", "packages-update": [ "@composer clear-cache", @@ -46,10 +42,13 @@ ] }, "config": { + "platform": { + "php": "8.3" + }, "allow-plugins": { "composer/*": true, "dealerdirect/phpcodesniffer-composer-installer": true, "phpstan/extension-installer": true } } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 7174006..eac18fa 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": "16e7026bf8fefb4b5d3bbc5997dbbfd3", + "content-hash": "2780f845ee16f3166dfb973a0fe5ee56", "packages": [ { "name": "eluceo/ical", @@ -72,16 +72,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -94,7 +94,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -119,7 +119,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -130,12 +130,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" } ], "packages-dev": [ @@ -352,16 +356,16 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", "shasum": "" }, "require": { @@ -444,33 +448,34 @@ "type": "thanks_dev" } ], - "time": "2025-11-11T04:32:07+00:00" + "time": "2026-05-06T08:26:05+00:00" }, { "name": "doctrine/instantiator", - "version": "2.1.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^8.4" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^14", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58" + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -497,7 +502,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -513,7 +518,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T06:47:08+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "johnbillion/wp-compat", @@ -894,16 +899,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.9.0", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a" + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", - "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", "shasum": "" }, "conflict": { @@ -914,9 +919,10 @@ "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.4.1", + "phpdocumentor/reflection-docblock": "^6.0", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, @@ -939,9 +945,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.0" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" }, - "time": "2025-12-03T23:06:24+00:00" + "time": "2026-02-03T19:29:21+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -1464,11 +1470,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.33", + "version": "2.1.54", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", - "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", "shasum": "" }, "require": { @@ -1513,25 +1519,25 @@ "type": "github" } ], - "time": "2025-12-05T10:24:31+00:00" + "time": "2026-04-29T13:31:09+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "468e02c9176891cc901143da118f09dc9505fc2f" + "reference": "6b5571001a7f04fa0422254c30a0017ec2f2cacc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", - "reference": "468e02c9176891cc901143da118f09dc9505fc2f", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/6b5571001a7f04fa0422254c30a0017ec2f2cacc", + "reference": "6b5571001a7f04fa0422254c30a0017ec2f2cacc", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.15" + "phpstan/phpstan": "^2.1.39" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -1556,29 +1562,32 @@ "MIT" ], "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.4" }, - "time": "2025-05-14T10:56:57+00:00" + "time": "2026-02-09T13:21:14+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.7", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" + "reference": "9b000a578b85b32945b358b172c7b20e91189024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024", + "reference": "9b000a578b85b32945b358b172c7b20e91189024", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.29" + "phpstan/phpstan": "^2.1.39" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -1604,11 +1613,14 @@ "MIT" ], "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11" }, - "time": "2025-09-26T11:19:08+00:00" + "time": "2026-05-02T06:54:10+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2149,18 +2161,18 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "95fda149b750941a5d7bd292e712107ca3227a04" + "reference": "16706d82a6f250e56047a9e95791a92a8a29f791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/95fda149b750941a5d7bd292e712107ca3227a04", - "reference": "95fda149b750941a5d7bd292e712107ca3227a04", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/16706d82a6f250e56047a9e95791a92a8a29f791", + "reference": "16706d82a6f250e56047a9e95791a92a8a29f791", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adaptcms/adaptcms": "<=1.3", - "admidio/admidio": "<=4.3.16", + "admidio/admidio": "<=5.0.8", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", @@ -2177,6 +2189,7 @@ "alextselegidis/easyappointments": "<=1.5.2", "alexusmai/laravel-file-manager": "<=3.3.1", "algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1", + "almirhodzic/nova-toggle-5": "<1.3", "alt-design/alt-redirect": "<1.6.4", "altcha-org/altcha": "<1.3.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", @@ -2185,6 +2198,7 @@ "amphp/artax": "<1.0.6|>=2,<2.0.6", "amphp/http": "<=1.7.2|>=2,<=2.1", "amphp/http-client": ">=4,<4.4", + "amphp/http-server": ">=2.0.0.0-RC1-dev,<2.1.10|>=3.0.0.0-beta1,<3.4.4", "anchorcms/anchor-cms": "<=0.12.7", "andreapollastri/cipi": "<=3.1.15", "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", @@ -2201,28 +2215,30 @@ "athlon1600/php-proxy": "<=5.1", "athlon1600/php-proxy-app": "<=3", "athlon1600/youtube-downloader": "<=4", + "aureuserp/aureuserp": "<1.3.0.0-beta1", "austintoddj/canvas": "<=3.4.2", - "auth0/auth0-php": ">=3.3,<8.18", - "auth0/login": "<7.20", - "auth0/symfony": "<=5.5", - "auth0/wordpress": "<=5.4", + "auth0/auth0-php": ">=3.3,<=8.18", + "auth0/login": "<=7.20", + "auth0/symfony": "<=5.7", + "auth0/wordpress": "<=5.5", "automad/automad": "<2.0.0.0-alpha5", "automattic/jetpack": "<9.8", "awesome-support/awesome-support": "<=6.0.7", - "aws/aws-sdk-php": "<3.368", - "azuracast/azuracast": "<=0.23.1", + "aws/aws-sdk-php": "<=3.371.3", + "ayacoo/redirect-tab": "<2.1.2|>=3,<3.1.7|>=4,<4.0.5", + "azuracast/azuracast": "<=0.23.5", "b13/seo_basics": "<0.8.2", "backdrop/backdrop": "<=1.32", "backpack/crud": "<3.4.9", "backpack/filemanager": "<2.0.2|>=3,<3.0.9", "bacula-web/bacula-web": "<9.7.1", "badaso/core": "<=2.9.11", - "bagisto/bagisto": "<2.3.10", + "bagisto/bagisto": "<=2.3.15", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", "barryvdh/laravel-translation-manager": "<0.6.8", "barzahlen/barzahlen-php": "<2.0.1", - "baserproject/basercms": "<=5.1.1", + "baserproject/basercms": "<=5.2.2", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", "bbpress/bbpress": "<2.6.5", "bcit-ci/codeigniter": "<3.1.3", @@ -2250,7 +2266,7 @@ "bytefury/crater": "<6.0.2", "cachethq/cachet": "<2.5.1", "cadmium-org/cadmium-cms": "<=0.4.9", - "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|>=5.2.10,<5.2.12|==5.3", "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", "cardgate/magento2": "<2.0.33", "cardgate/woocommerce": "<=3.1.15", @@ -2261,15 +2277,17 @@ "causal/oidc": "<4", "cecil/cecil": "<7.47.1", "centreon/centreon": "<22.10.15", + "cesargb/laravel-magiclink": ">=2,<2.25.1", "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", "chrome-php/chrome": "<1.14", + "ci4-cms-erp/ci4ms": "<=0.31.7", "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", "ckeditor/ckeditor": "<4.25", "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", "co-stack/fal_sftp": "<0.2.6", - "cockpit-hq/cockpit": "<2.11.4", - "code16/sharp": "<9.11.1", + "cockpit-hq/cockpit": "<2.14", + "code16/sharp": "<9.20", "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<3.1.10", "codeigniter4/framework": "<4.6.2", @@ -2279,8 +2297,8 @@ "codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5", "commerceteam/commerce": ">=0.9.6,<0.9.9", "components/jquery": ">=1.0.3,<3.5", - "composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3", - "concrete5/concrete5": "<9.4.3", + "composer/composer": "<2.2.28|>=2.3,<2.9.8", + "concrete5/concrete5": "<9.4.8", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", @@ -2289,11 +2307,19 @@ "contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5", "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", - "coreshop/core-shop": "<=4.1.7", + "coreshop/core-shop": "<4.1.9|==5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", "couleurcitron/tarteaucitron-wp": "<0.3", - "craftcms/cms": "<=4.16.16|>=5,<=5.8.20", + "cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2", + "craftcms/aws-s3": ">=2.0.2,<=2.2.4", + "craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1", + "craftcms/cms": "<4.17.12|>=5,<5.9.18", + "craftcms/commerce": ">=4,<4.11|>=5,<5.6", + "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1", + "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21", + "craftcms/google-cloud": ">=2.0.0.0-beta1,<=2.2", + "craftcms/webhooks": ">=3,<3.2", "croogo/croogo": "<=4.0.7", "cuyz/valinor": "<0.12", "czim/file-handling": "<1.5|>=2,<2.3", @@ -2307,14 +2333,16 @@ "david-garcia/phpwhois": "<=4.3.1", "dbrisinajumi/d2files": "<1", "dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta", + "dedoc/scramble": ">=0.13.2,<0.13.22", "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", "desperado/xml-bundle": "<=0.1.7", "dev-lancer/minecraft-motd-parser": "<=1.0.5", - "devcode-it/openstamanager": "<=2.9.4", + "devcode-it/openstamanager": "<=2.10.1", "devgroup/dotplant": "<2020.09.14-dev", "digimix/wp-svg-upload": "<=1", "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "directorytree/imapengine": "<1.22.3", "dl/yag": "<3.0.1", "dmk/webkitpdf": "<1.1.4", "dnadesign/silverstripe-elemental": "<5.3.12", @@ -2327,9 +2355,10 @@ "doctrine/mongodb-odm": "<1.0.2", "doctrine/mongodb-odm-bundle": "<3.0.1", "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<21.0.3", + "dolibarr/dolibarr": "<=23.0.2", "dompdf/dompdf": "<2.0.4", "doublethreedigital/guest-entries": "<3.1.2", + "dreamfactory/df-core": "<1.0.4", "drupal-pattern-lab/unified-twig-extensions": "<=0.1", "drupal/access_code": "<2.0.5", "drupal/acquia_dam": "<1.1.5", @@ -2368,10 +2397,10 @@ "drupal/umami_analytics": "<1.0.1", "duncanmcclean/guest-entries": "<3.1.2", "dweeves/magmi": "<=0.7.24", - "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.3.1", "ecodev/newsletter": "<=4", "ectouch/ectouch": "<=2.7.2", - "egroupware/egroupware": "<23.1.20240624", + "egroupware/egroupware": "<23.1.20260113|>=26.0.20251208,<26.0.20260113", "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", "elijaa/phpmemcacheadmin": "<=1.3", @@ -2394,18 +2423,18 @@ "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5", "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", "ezsystems/ezplatform-http-cache": "<2.3.16", - "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", + "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<1.3.35", "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", "ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40", "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", "ezsystems/ezplatform-user": ">=1,<1.0.1", - "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<7.5.31", "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", "ezyang/htmlpurifier": "<=4.2", "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", - "facturascripts/facturascripts": "<=2025.4|==2025.11|==2025.41|==2025.43", + "facturascripts/facturascripts": "<=2025.92|>=2026,<=2026.1", "fastly/magento2": "<1.2.26", "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", @@ -2413,20 +2442,22 @@ "filament/actions": ">=3.2,<3.2.123", "filament/filament": ">=4,<4.3.1", "filament/infolists": ">=3,<3.2.115", - "filament/tables": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115|>=4,<4.8.5|>=5,<5.3.5", "filegator/filegator": "<7.8", "filp/whoops": "<2.1.13", "fineuploader/php-traditional-server": "<=1.2.2", - "firebase/php-jwt": "<6", + "firebase/php-jwt": "<7", "fisharebest/webtrees": "<=2.1.18", "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", - "flarum/core": "<1.8.10", + "flarum/core": "<=1.8.15|>=2.0.0.0-beta1,<=2.0.0.0-beta8", "flarum/flarum": "<0.1.0.0-beta8", "flarum/framework": "<1.8.10", "flarum/mentions": "<1.6.3", + "flarum/nicknames": "<1.8.3", "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", "flarum/tags": "<=0.1.0.0-beta13", + "flightphp/core": "<3.18.1", "floriangaerber/magnesium": "<0.3.1", "fluidtypo3/vhs": "<5.1.1", "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", @@ -2446,17 +2477,20 @@ "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", "froala/wysiwyg-editor": "<=4.3", - "froxlor/froxlor": "<=2.2.5", + "frosh/adminer-platform": "<2.2.1", + "froxlor/froxlor": "<2.3.6", "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", - "funadmin/funadmin": "<=5.0.2", + "funadmin/funadmin": "<=7.1.0.0-RC6", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", "georgringer/news": "<1.3.3", "geshi/geshi": "<=1.0.9.1", - "getformwork/formwork": "<2.2", - "getgrav/grav": "<1.11.0.0-beta1", - "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<=5.2.1", + "getformwork/formwork": "<=2.3.3", + "getgrav/grav": "<=2.0.0.0-RC1", + "getgrav/grav-plugin-api": "<1.0.0.0-beta15", + "getgrav/grav-plugin-form": "<9.1", + "getkirby/cms": "<4.9|>=5,<5.4", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -2465,12 +2499,13 @@ "globalpayments/php-sdk": "<2", "goalgorilla/open_social": "<12.3.11|>=12.4,<12.4.10|>=13.0.0.0-alpha1,<13.0.0.0-alpha11", "gogentooss/samlbase": "<1.2.7", - "google/protobuf": "<3.4", + "goodoneuz/pay-uz": "<=2.2.24", + "google/protobuf": "<4.33.6", "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", "gp247/core": "<1.1.24", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<6.1.17", + "grumpydictator/firefly-iii": "<6.1.17|>=6.4.23,<=6.5", "gugoan/economizzer": "<=0.9.0.0-beta1", "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", "guzzlehttp/oauth-subscriber": "<0.8.1", @@ -2485,6 +2520,7 @@ "hjue/justwriting": "<=1", "hov/jobfair": "<1.0.13|>=2,<2.0.2", "httpsoft/http-message": "<1.0.12", + "hybridauth/hybridauth": "<=3.12.2", "hyn/multi-tenant": ">=5.6,<5.7.2", "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.25|>=5,<5.0.3", "ibexa/admin-ui-assets": ">=4.6.0.0-alpha1,<4.6.21", @@ -2496,7 +2532,7 @@ "ibexa/solr": ">=4.5,<4.5.4", "ibexa/user": ">=4,<4.4.3|>=5,<5.0.4", "icecoder/icecoder": "<=8.1", - "idno/known": "<=1.3.1", + "idno/known": "<1.6.4", "ilicmiljan/secure-props": ">=1.2,<1.2.2", "illuminate/auth": "<5.5.10", "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", @@ -2513,10 +2549,13 @@ "innologi/typo3-appointments": "<2.0.6", "intelliants/subrion": "<4.2.2", "inter-mediator/inter-mediator": "==5.5", - "ipl/web": "<0.10.1", + "intercom/intercom-php": "==5.0.2", + "invoiceninja/invoiceninja": "<5.13.4", + "ipl/web": "<=0.13", "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", + "j0k3r/graby": "<=2.5", "jackalope/jackalope-doctrine-dbal": "<1.7.4", "jambagecom/div2007": "<0.10.2", "james-heinrich/getid3": "<1.9.21", @@ -2524,7 +2563,9 @@ "jasig/phpcas": "<1.3.3", "jbartels/wec-map": "<3.0.3", "jcbrand/converse.js": "<3.3.3", + "joedolson/my-calendar": "<3.7.7", "joelbutcher/socialstream": "<5.6|>=6,<6.2", + "johnbillion/query-monitor": "<3.20.4", "johnbillion/wp-crontrol": "<1.16.2|>=1.17,<1.19.2", "joomla/application": "<1.0.13", "joomla/archive": "<1.1.12|>=2,<2.0.1", @@ -2542,17 +2583,19 @@ "juzaweb/cms": "<=3.4.2", "jweiland/events2": "<8.3.8|>=9,<9.0.6", "jweiland/kk-downloader": "<1.2.2", + "kantorge/yaffa": "<=2", "kazist/phpwhois": "<=4.2.6", + "kelvinmo/simplejwt": "<=1.1", "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", - "khodakhah/nodcms": "<=3", - "kimai/kimai": "<=2.20.1", + "khodakhah/nodcms": "<=3.4.1", + "kimai/kimai": "<=2.55", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", "knplabs/knp-snappy": "<=1.4.2", "kohana/core": "<3.3.3", "koillection/koillection": "<1.6.12", - "krayin/laravel-crm": "<=1.3", + "krayin/laravel-crm": "<=2.2", "kreait/firebase-php": ">=3.2,<3.8.1", "kumbiaphp/kumbiapp": "<=1.1.1", "la-haute-societe/tcpdf": "<6.2.22", @@ -2564,24 +2607,26 @@ "laravel/fortify": "<1.11.1", "laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1", "laravel/laravel": ">=5.4,<5.4.22", + "laravel/passport": ">=13,<13.7.1", "laravel/pulse": "<1.3.1", - "laravel/reverb": "<1.4", + "laravel/reverb": "<1.7", "laravel/socialite": ">=1,<2.0.10", "latte/latte": "<2.10.8", - "lavalite/cms": "<=9|==10.1", + "lavalite/cms": "<=10.1", "lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2", "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", - "league/commonmark": "<2.7", + "league/commonmark": "<=2.8.1", "league/flysystem": "<1.1.4|>=2,<2.1.1", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", "libreform/libreform": ">=2,<=2.0.8", - "librenms/librenms": "<25.12", + "librenms/librenms": "<26.3", "liftkit/database": "<2.13.2", "lightsaml/lightsaml": "<1.3.5", - "limesurvey/limesurvey": "<6.5.12", + "limesurvey/limesurvey": "<6.15.4", "livehelperchat/livehelperchat": "<=3.91", + "livewire-filemanager/filemanager": "<=1.0.4", "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4", "livewire/volt": "<1.7", "lms/routes": "<2.1.1", @@ -2602,15 +2647,17 @@ "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", "manogi/nova-tiptap": "<=3.2.6", - "mantisbt/mantisbt": "<2.27.2", + "mantisbt/mantisbt": "<2.28.2", "marcwillmann/turn": "<0.3.3", + "markhuot/craftql": "<=1.3.7", "marshmallow/nova-tiptap": "<5.7", "matomo/matomo": "<1.11", "matyhtf/framework": "<3.0.6", - "mautic/core": "<5.2.9|>=6,<6.0.7", + "mautic/core": "<5.2.10|>=6,<6.0.8|>=7.0.0.0-alpha,<7.0.1", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7", "maximebf/debugbar": "<1.19", + "mckenziearts/livewire-markdown-editor": "<1.3", "mdanter/ecc": "<2", "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", "mediawiki/cargo": "<3.8.3", @@ -2629,18 +2676,20 @@ "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", "microsoft/microsoft-graph-beta": "<2.0.1", "microsoft/microsoft-graph-core": "<2.0.2", - "microweber/microweber": "<=2.0.19", + "microweber/microweber": "<2.0.20", "mikehaertl/php-shellcommand": "<1.6.1", "mineadmin/mineadmin": "<=3.0.9", "miniorange/miniorange-saml": "<1.4.3", + "miraheze/ts-portal": "<=33", "mittwald/typo3_forum": "<1.2.1", + "mix/mix": ">=2,<=2.2.17", "mobiledetect/mobiledetectlib": "<2.8.32", "modx/revolution": "<=3.1", "mojo42/jirafeau": "<4.4", "mongodb/mongodb": ">=1,<1.9.2", "mongodb/mongodb-extension": "<1.21.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<4.4.11|>=4.5.0.0-beta,<4.5.7|>=5.0.0.0-beta,<5.0.3", + "moodle/moodle": "<4.5.9|>=5.0.0.0-beta,<5.0.5|>=5.1.0.0-beta,<5.1.2", "moonshine/moonshine": "<=3.12.5", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", @@ -2653,6 +2702,7 @@ "munkireport/softwareupdate": "<1.6", "mustache/mustache": ">=2,<2.14.1", "mwdelaney/wp-enable-svg": "<=0.2", + "nabeel/phpvms": "<7.0.6", "namshi/jose": "<2.2", "nasirkhan/laravel-starter": "<11.11", "nategood/httpful": "<1", @@ -2683,9 +2733,9 @@ "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", "october/backend": "<1.1.2", "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", - "october/october": "<3.7.5", - "october/rain": "<1.0.472|>=1.1,<1.1.2", - "october/system": "<=3.7.12|>=4,<=4.0.11", + "october/october": "<3.7.14|>=4,<4.1.10", + "october/rain": "<=3.7.13|>=4,<=4.1.9", + "october/system": "<3.7.16|>=4,<4.1.16", "oliverklee/phpunit": "<3.5.15", "omeka/omeka-s": "<4.0.3", "onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1", @@ -2693,9 +2743,9 @@ "open-web-analytics/open-web-analytics": "<1.8.1", "opencart/opencart": ">=0", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<20.16", + "openmage/magento-lts": "<=20.17", "opensolutions/vimbadmin": "<=3.0.15", - "opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7", + "opensource-workshop/connect-cms": "<1.41.1|>=2,<2.41.1", "orchid/platform": ">=8,<14.43", "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", @@ -2725,6 +2775,7 @@ "pear/pear": "<=1.10.1", "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", "personnummer/personnummer": "<3.0.2", + "ph7software/ph7builder": "<=17.9.1", "phanan/koel": "<5.1.4", "phenx/php-svg-lib": "<0.5.2", "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", @@ -2735,19 +2786,20 @@ "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", "phpmyadmin/phpmyadmin": "<5.2.2", - "phpmyfaq/phpmyfaq": "<=4.0.13", + "phpmyfaq/phpmyfaq": "<=4.1.1", "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", - "phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", + "phpoffice/phpspreadsheet": "<=1.30.3|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", "phppgadmin/phppgadmin": "<=7.13", - "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", + "phpseclib/phpseclib": "<=2.0.53|>=3,<=3.0.51", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", - "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8|>=12.5.21,<12.5.22|>=13.1.5,<13.1.6", "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", + "phraseanet/phraseanet": "==4.0.3", "pi/pi": "<=2.5", "pimcore/admin-ui-classic-bundle": "<=1.7.15|>=2.0.0.0-RC1-dev,<=2.2.2", "pimcore/customer-management-framework-bundle": "<4.2.1", @@ -2756,13 +2808,13 @@ "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<=11.5.13|>=12.0.0.0-RC1-dev,<12.3.1", + "pimcore/pimcore": "<=11.5.14.1|>=12,<12.3.3|==12.3.3", "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1", "piwik/piwik": "<1.11", "pixelfed/pixelfed": "<0.12.5", "plotly/plotly.js": "<2.25.2", "pocketmine/bedrock-protocol": "<8.0.2", - "pocketmine/pocketmine-mp": "<5.32.1", + "pocketmine/pocketmine-mp": "<5.42.1", "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", @@ -2770,23 +2822,25 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.2.3", + "prestashop/prestashop": "<8.2.6|>=9,<9.1.1", "prestashop/productcomments": "<5.0.2", - "prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5", + "prestashop/ps_checkout": "<5.3", "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", "prestashop/ps_linklist": "<3.1", "privatebin/privatebin": "<1.4|>=1.5,<1.7.4|>=1.7.7,<2.0.3", - "processwire/processwire": "<=3.0.246", + "processwire/processwire": "<=3.0.255", "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", - "pterodactyl/panel": "<1.12", + "psy/psysh": "<=0.11.22|>=0.12,<=0.12.18", + "pterodactyl/panel": "<1.12.1", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", "ptrofimov/beanstalk_console": "<1.7.14", "pubnub/pubnub": "<6.1", "punktde/pt_extbase": "<1.5.1", "pusher/pusher-php-server": "<2.2.1", + "putyourlightson/craft-sprig": ">=2,<2.15.2|>=3,<3.7.2", "pwweb/laravel-core": "<=0.3.6.0-beta", "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", "pyrocms/pyrocms": "<=3.9.1", @@ -2795,25 +2849,30 @@ "rainlab/blog-plugin": "<1.4.1", "rainlab/debugbar-plugin": "<3.1", "rainlab/user-plugin": "<=1.4.5", + "ralffreit/mfa-email": "<1.0.7|==2", "rankmath/seo-by-rank-math": "<=1.0.95", "rap2hpoutre/laravel-log-viewer": "<0.13", "react/http": ">=0.7,<1.9", "really-simple-plugins/complianz-gdpr": "<6.4.2", - "redaxo/source": "<=5.20.1", + "redaxo/source": "<5.21", "remdex/livehelperchat": "<4.29", "renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1", "reportico-web/reportico": "<=8.1", - "rhukster/dom-sanitizer": "<1.0.7", + "rhukster/dom-sanitizer": "<1.0.10", "rmccue/requests": ">=1.6,<1.8", - "robrichards/xmlseclibs": "<=3.1.3", + "roadiz/documents": "<2.3.42|>=2.4,<2.5.44|>=2.6,<2.6.28|>=2.7,<2.7.9", + "roadiz/openid": "<2.3.43|>=2.5,<2.5.45|>=2.6,<2.6.31|>=2.7,<2.7.18", + "robrichards/xmlseclibs": "<3.1.5", "roots/soil": "<4.1", - "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11", + "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11|>=1.7.0.0-beta,<1.7.0.0-RC5-dev", "rudloff/alltube": "<3.0.3", "rudloff/rtmpdump-bin": "<=2.3.1", "s-cart/core": "<=9.0.5", "s-cart/s-cart": "<6.9", + "s9y/serendipity": "<2.6", "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "saloonphp/saloon": "<4", "samwilson/unlinked-wikibase": "<1.42", "scheb/two-factor-bundle": "<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", @@ -2821,17 +2880,17 @@ "setasign/fpdi": "<2.6.4", "sfroemken/url_redirect": "<=1.2.1", "sheng/yiicms": "<1.2.1", - "shopware/core": "<6.6.10.9-dev|>=6.7,<6.7.6.1-dev", - "shopware/platform": "<6.6.10.7-dev|>=6.7,<6.7.3.1-dev", + "shopware/core": "<6.6.10.15-dev|>=6.7,<6.7.8.1-dev", + "shopware/platform": "<6.6.10.15-dev|>=6.7,<6.7.8.1-dev", "shopware/production": "<=6.3.5.2", "shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev", "shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev", "shopxo/shopxo": "<=6.4", - "showdoc/showdoc": "<2.10.4", + "showdoc/showdoc": "<3.8.1", "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", - "silverstripe/assets": ">=1,<1.11.1", + "silverstripe/assets": "<2.4.5|>=3,<3.1.3", "silverstripe/cms": "<4.11.3", "silverstripe/comments": ">=1.3,<3.1.1", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", @@ -2856,7 +2915,7 @@ "simplesamlphp/simplesamlphp-module-openid": "<1", "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", "simplesamlphp/xml-common": "<1.20", - "simplesamlphp/xml-security": "==1.6.11", + "simplesamlphp/xml-security": "<1.13.9|>=2,<2.3.1", "simplito/elliptic-php": "<1.0.6", "sitegeist/fluid-components": "<3.5", "sjbr/sr-feuser-register": "<2.6.2|>=5.1,<12.5", @@ -2866,10 +2925,10 @@ "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<=8.3.4", + "snipe/snipe-it": "<8.4.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", - "solspace/craft-freeform": "<4.1.29|>=5,<5.10.16", + "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6", "soosyze/soosyze": "<=2", "spatie/browsershot": "<5.0.5", "spatie/image-optimizer": "<1.7.3", @@ -2884,14 +2943,14 @@ "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", "starcitizenwiki/embedvideo": "<=4", - "statamic/cms": "<=5.22", + "statamic/cms": "<5.73.21|>=6,<6.15", "stormpath/sdk": "<9.9.99", - "studio-42/elfinder": "<=2.1.64", + "studio-42/elfinder": "<=2.1.67", "studiomitte/friendlycaptcha": "<0.1.4", "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", "sulu/form-bundle": ">=2,<2.5.3", - "sulu/sulu": "<1.6.44|>=2,<2.5.25|>=2.6,<2.6.9|>=3.0.0.0-alpha1,<3.0.0.0-alpha3", + "sulu/sulu": "<2.6.22|>=3,<3.0.5", "sumocoders/framework-user-bundle": "<1.4", "superbig/craft-audit": "<3.0.2", "svewap/a21glossary": "<=0.4.10", @@ -2903,7 +2962,7 @@ "sylius/grid-bundle": "<1.10.1", "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", - "sylius/sylius": "<1.12.19|>=1.13.0.0-alpha1,<1.13.4", + "sylius/sylius": "<1.9.12|>=1.10,<1.10.16|>=1.11,<1.11.17|>=1.12,<=1.12.22|>=1.13,<=1.13.14|>=1.14,<=1.14.17|>=2,<=2.0.15|>=2.1,<=2.1.11|>=2.2,<=2.2.2", "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", @@ -2923,7 +2982,7 @@ "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/polyfill": ">=1,<1.10", "symfony/polyfill-php55": ">=1,<1.10", - "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/process": "<5.4.51|>=6,<6.4.33|>=7,<7.1.7|>=7.3,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5", "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/routing": ">=2,<2.0.19", "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", @@ -2934,7 +2993,7 @@ "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", - "symfony/symfony": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7", + "symfony/symfony": "<5.4.51|>=6,<6.4.33|>=7,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5", "symfony/translation": ">=2,<2.0.17", "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", "symfony/ux-autocomplete": "<2.11.2", @@ -2958,7 +3017,7 @@ "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<6.0.8", - "thorsten/phpmyfaq": "<4.0.16|>=4.1.0.0-alpha,<=4.1.0.0-beta2", + "thorsten/phpmyfaq": "<=4.1.1", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", "tinymce/tinymce": "<7.2", @@ -2976,8 +3035,9 @@ "ttskch/pagination-service-provider": "<1", "twbs/bootstrap": "<3.4.1|>=4,<4.3.1", "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", + "typicms/core": "<16.1.7", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1|==14.2", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", @@ -3023,28 +3083,30 @@ "vertexvaar/falsftp": "<0.2.6", "villagedefrance/opencart-overclocked": "<=1.11.1", "vova07/yii2-fileapi-widget": "<0.1.9", - "vrana/adminer": "<=4.8.1", + "vrana/adminer": "<5.4.2", "vufind/vufind": ">=2,<9.1.1", "waldhacker/hcaptcha": "<2.1.2", "wallabag/tcpdf": "<6.2.22", "wallabag/wallabag": "<2.6.11", "wanglelecc/laracms": "<=1.0.3", "wapplersystems/a21glossary": "<=0.4.10", - "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9", - "web-auth/webauthn-lib": ">=4.5,<4.9", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9|>=5.2,<5.2.4|>=5.3,<5.3.1", + "web-auth/webauthn-lib": ">=4.5,<4.9|>=5.2,<5.2.4", + "web-auth/webauthn-symfony-bundle": ">=5.2,<5.2.4", "web-feet/coastercms": "==5.5", "web-tp3/wec_map": "<3.0.3", "webbuilders-group/silverstripe-kapost-bridge": "<0.4", "webcoast/deferred-image-processing": "<1.0.2", "webklex/laravel-imap": "<5.3", "webklex/php-imap": "<5.3", + "webonyx/graphql-php": "<=15.32.2", "webpa/webpa": "<3.1.2", "webreinvent/vaahcms": "<=2.3.1", "wikibase/wikibase": "<=1.39.3", "wikimedia/parsoid": "<0.12.2", "willdurand/js-translation-bundle": "<2.1.1", - "winter/wn-backend-module": "<1.2.4", - "winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7", + "winter/wn-backend-module": "<1.2.12", + "winter/wn-cms-module": "<=1.2.9", "winter/wn-dusk-plugin": "<2.1", "winter/wn-system-module": "<1.2.4", "wintercms/winter": "<=1.2.3", @@ -3056,16 +3118,18 @@ "wpanel/wpanel4-cms": "<=4.3.1", "wpcloud/wp-stateless": "<3.2", "wpglobus/wpglobus": "<=1.9.6", - "wwbn/avideo": "<14.3", + "wpmetabox/meta-box": "<5.11.2", + "wwbn/avideo": "<=29", "xataface/xataface": "<3", "xpressengine/xpressengine": "<3.0.15", "yab/quarx": "<2.4.5", - "yeswiki/yeswiki": "<=4.5.4", + "yansongda/pay": "<=3.7.19", + "yeswiki/yeswiki": "<=4.6", "yetiforce/yetiforce-crm": "<6.5", "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", "yiisoft/yii": "<1.1.31", - "yiisoft/yii2": "<2.0.52", + "yiisoft/yii2": "<2.0.55", "yiisoft/yii2-authclient": "<2.2.15", "yiisoft/yii2-bootstrap": "<2.0.4", "yiisoft/yii2-dev": "<=2.0.45", @@ -3075,6 +3139,7 @@ "yiisoft/yii2-redis": "<2.0.20", "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", "yoast-seo-for-typo3/yoast_seo": "<7.2.3", + "yoast/duplicate-post": "<=4.5", "yourls/yourls": "<=1.10.2", "yuan1994/tpadmin": "<=1.3.12", "yungifez/skuul": "<=2.6.5", @@ -3115,7 +3180,8 @@ "zf-commons/zfc-user": "<1.2.2", "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", "zfr/zfr-oauth2-server-module": "<0.1.2", - "zoujingli/thinkadmin": "<=6.1.53" + "zoujingli/thinkadmin": "<=6.1.53", + "zumba/json-serializer": "<3.2.3" }, "default-branch": true, "type": "metapackage", @@ -3153,7 +3219,7 @@ "type": "tidelift" } ], - "time": "2026-01-15T23:06:28+00:00" + "time": "2026-05-14T13:38:25+00:00" }, { "name": "sebastian/cli-parser", @@ -4299,16 +4365,16 @@ }, { "name": "symfony/config", - "version": "v7.4.3", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace" + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/800ce889e358a53a9678b3212b0c8cecd8c6aace", - "reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace", + "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", "shasum": "" }, "require": { @@ -4354,7 +4420,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.3" + "source": "https://github.com/symfony/config/tree/v7.4.10" }, "funding": [ { @@ -4374,20 +4440,20 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:24:27+00:00" + "time": "2026-05-03T14:20:49+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.3", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "54122901b6d772e94f1e71a75e0533bc16563499" + "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54122901b6d772e94f1e71a75e0533bc16563499", - "reference": "54122901b6d772e94f1e71a75e0533bc16563499", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", "shasum": "" }, "require": { @@ -4438,7 +4504,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.3" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.10" }, "funding": [ { @@ -4458,20 +4524,20 @@ "type": "tidelift" } ], - "time": "2025-12-28T10:55:46+00:00" + "time": "2026-05-06T11:55:30+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { @@ -4508,7 +4574,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -4528,20 +4594,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -4591,7 +4657,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -4611,20 +4677,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -4676,7 +4742,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -4696,20 +4762,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -4727,7 +4793,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4763,7 +4829,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -4783,20 +4849,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701", + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701", "shasum": "" }, "require": { @@ -4844,7 +4910,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.9" }, "funding": [ { @@ -4864,7 +4930,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -5199,7 +5265,10 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.9.0" + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "8.3" + }, + "plugin-api-version": "2.2.0" } diff --git a/package-lock.json b/package-lock.json index 02f3dd3..b89fce2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,11 @@ "ajv": "^8.17.1" }, "devDependencies": { + "@playwright/test": "^1.50.0", "@woocommerce/components": "^12.3.0", "@wordpress/env": "^10.0.0", "@wordpress/scripts": "^29.0.0", + "dotenv": "^16.4.0", "md5": "^2.3.0", "npm-run-all": "^4.1.5" }, @@ -6989,6 +6991,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz", @@ -17361,6 +17379,19 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/downshift": { "version": "6.1.12", "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz", @@ -25815,6 +25846,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plur": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", diff --git a/package.json b/package.json index b61d718..baf38ef 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ "npm": ">=10.2" }, "devDependencies": { + "@playwright/test": "^1.50.0", "@woocommerce/components": "^12.3.0", "@wordpress/env": "^10.0.0", "@wordpress/scripts": "^29.0.0", + "dotenv": "^16.4.0", "md5": "^2.3.0", "npm-run-all": "^4.1.5" }, @@ -45,7 +47,10 @@ "lint:md:js": "wp-scripts lint-md-js", "lint:pkg-json": "wp-scripts lint-pkg-json", "packages-update": "wp-scripts packages-update", - "test:e2e": "wp-scripts test-e2e", + "test:e2e": "playwright test --config=tests/e2e/playwright.config.js", + "test:e2e:ui": "playwright test --config=tests/e2e/playwright.config.js --ui", + "test:e2e:headed": "playwright test --config=tests/e2e/playwright.config.js --headed", + "test:e2e:debug": "playwright test --config=tests/e2e/playwright.config.js --debug", "test:php": "wp-env run tests-cli --env-cwd='wp-content/plugins/simple-events' -- vendor/bin/phpunit --configuration phpunit.xml.dist", "test:unit": "wp-scripts test-unit-js", "wp-env": "wp-env", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 622d997..0be1bfa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,7 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" + testdox="true" > diff --git a/src/blocks/event-info/index.js b/src/blocks/event-info/index.js index bd012c1..23016e4 100644 --- a/src/blocks/event-info/index.js +++ b/src/blocks/event-info/index.js @@ -38,7 +38,6 @@ import { useBlockProps, } from '@wordpress/block-editor'; import { useEntityProp } from '@wordpress/core-data'; -import { useSelect, useDispatch } from '@wordpress/data'; // Import date utilities import { @@ -178,11 +177,10 @@ export const autoSaveEventDates = async (dates, dateManagerInstance = null) => { }; /** - * Saves event dates when the post is being saved. + * Saves event dates as part of a user-initiated save. * - * Handles saving event dates during WordPress post save operations. - * Also updates block attributes to ensure the saved dates with IDs - * are properly persisted in the block editor. + * Thin wrapper around `saveEventDates`; the Save-click interceptor in `edit()` + * handles the attribute write before `savePost`. * * @since 2.0.0 * @@ -191,33 +189,7 @@ export const autoSaveEventDates = async (dates, dateManagerInstance = null) => { * @return {Promise} Promise that resolves to the saved event dates response. */ export const saveEventDatesOnPostSave = async (dates, dateManagerInstance = null) => { - try { - // Save to REST API - const savedDates = await saveEventDates(dates, dateManagerInstance); - - // Update the post meta to ensure the updated dates (with IDs) are persisted - const postId = window?.wp?.data?.select('core/editor')?.getCurrentPostId(); - if (postId && savedDates) { - - // Update the block attributes to ensure the updated dates (with IDs) are persisted - const blocks = window.wp.data.select('core/block-editor').getBlocks(); - const eventInfoBlock = blocks.find(block => block.name === 'simple-events/event-info'); - - if (eventInfoBlock) { - window.wp.data.dispatch('core/block-editor').updateBlockAttributes( - eventInfoBlock.clientId, - { - eventDates: savedDates.dates || savedDates, - } - ); - } - } - - return savedDates; - } catch (error) { - console.error('Error saving event dates on post save:', error); - throw error; - } + return saveEventDates(dates, dateManagerInstance); }; @@ -314,75 +286,71 @@ registerBlockType('simple-events/event-info', { // Add refresh counter to force re-renders when dateManager state changes const [refreshCounter, setRefreshCounter] = useState(0); - // Watch for post save events - const { isSavingPost, isAutosavingPost } = useSelect((select) => { - const { isSavingPost, isAutosavingPost } = select('core/editor'); - return { - isSavingPost: isSavingPost(), - isAutosavingPost: isAutosavingPost(), - }; - }, []); - - // Save dates before post save begins + // Sync dates before the post PUT, not after — a post-save setAttributes races Gutenberg and pins isEditedPostDirty() true forever. useEffect(() => { - let wasSaving = false; - let dateSavePromise = null; - - const unsubscribe = window.wp.data.subscribe(() => { - const { isSavingPost, isAutosavingPost } = window.wp.data.select('core/editor'); - const currentSaving = isSavingPost(); - const currentAutosaving = isAutosavingPost(); - - // Detect when save is about to start (transition from false to true) - if (currentSaving && !currentAutosaving && !wasSaving && dateManagerState?.getCurrentDates()?.dates) { - // Save dates immediately before post save continues - dateSavePromise = saveEventDatesOnPostSave(dateManagerState.getCurrentDates().dates, dateManagerState) - .then((savedDates) => { - if (savedDates && savedDates.dates) { - setAttributes({ - eventDates: savedDates.dates - }); - } - dateSavePromise = null; - }) - .catch((error) => { - console.error('Failed to save event dates before post save:', error); - dateSavePromise = null; - }); - } + if (!dateManagerState) { + return; + } + + let inflightSync = null; - // Detect when save finishes but date sync is still in progress - if (!currentSaving && wasSaving && dateSavePromise) { - // Wait for date sync to complete, then save again - dateSavePromise.then(() => { - // Trigger another post save to include the updated dates - window.wp.data.dispatch('core/editor').savePost(); - }); + const runSyncThenSave = async (event) => { + if (!dateManagerState.getCurrentDates().isDirty) { + return; } - wasSaving = currentSaving; - }); + event.preventDefault(); + event.stopImmediatePropagation(); - return () => unsubscribe(); - }, [dateManagerState, setAttributes]); + const isPanelConfirm = !!event.target.closest('.editor-post-publish-panel'); - // Sync dateManagerState dates to block attributes - useEffect(() => { - if (dateManagerState?.getCurrentDates()?.dates) { - // Get current block attributes - const currentEventDates = attributes.eventDates || []; - const newEventDates = dateManagerState.getCurrentDates().dates; + try { + if (!inflightSync) { + inflightSync = saveEventDates( + dateManagerState.getCurrentDates().dates, + dateManagerState + ); + } + const savedDates = await inflightSync; + inflightSync = null; + + if (savedDates && savedDates.dates) { + setAttributes({ eventDates: savedDates.dates }); + } - // Compare the dates to see if they're actually different - const datesChanged = JSON.stringify(currentEventDates) !== JSON.stringify(newEventDates); + // Panel confirm needs the status flip too; an Update click on a published post just needs savePost. + if (isPanelConfirm) { + window.wp.data.dispatch('core/editor').editPost({ status: 'publish' }); + } + window.wp.data.dispatch('core/editor').savePost(); + } catch (error) { + inflightSync = null; + console.error('Failed to sync event dates before save:', error); + } + }; - if (datesChanged) { - setAttributes({ - eventDates: newEventDates - }); + const onClickCapture = (event) => { + const button = event.target.closest('.editor-post-publish-button'); + if (!button) { + return; } - } - }, [dateManagerState, refreshCounter, setAttributes, attributes.eventDates]); + // Skip the toolbar Publish click that only opens the panel; intercept the real save (panel confirm, or Update on a published post). + const status = window.wp.data + .select('core/editor') + .getCurrentPostAttribute('status'); + const isPublished = ['publish', 'private', 'future'].includes(status); + const isInPanel = !!event.target.closest('.editor-post-publish-panel'); + if (!isPublished && !isInPanel) { + return; + } + runSyncThenSave(event); + }; + + document.addEventListener('click', onClickCapture, true); + return () => { + document.removeEventListener('click', onClickCapture, true); + }; + }, [dateManagerState, setAttributes]); // Check if we should be in edit mode based on missing data useEffect(() => { diff --git a/src/classes/class-se-event-post-type.php b/src/classes/class-se-event-post-type.php index 26e634c..b3d803b 100644 --- a/src/classes/class-se-event-post-type.php +++ b/src/classes/class-se-event-post-type.php @@ -58,6 +58,7 @@ public static function init() { add_filter( 'get_the_archive_title', array( __CLASS__, 'the_archive_title' ) ); register_activation_hook( __FILE__, array( __CLASS__, 'flush_rewrite_rules' ) ); add_action( 'save_post', array( __CLASS__, 'delete_event_dates_if_no_event_info_block' ) ); + add_action( 'transition_post_status', array( __CLASS__, 'stamp_version_on_new_event' ), 10, 3 ); add_action( 'before_delete_post', array( __CLASS__, 'delete_child_event_dates' ) ); add_action( 'wp_trash_post', array( __CLASS__, 'trash_child_event_dates' ) ); add_action( 'untrashed_post', array( __CLASS__, 'untrash_child_event_dates' ) ); @@ -706,6 +707,38 @@ public static function flush_rewrite_rules() { flush_rewrite_rules(); } + /** + * Stamp the current migration version on a brand-new event. + * + * Without this, an event saved before any date /sync fired had no + * se_event_version meta and was wrongly flagged for migration. Legacy + * events transition from publish/draft (never auto-draft/new) so they + * stay unstamped and migration still picks them up. + * + * @param string $new_status The new post status. + * @param string $old_status The old post status. + * @param WP_Post $post The post object. + * + * @return void + */ + public static function stamp_version_on_new_event( $new_status, $old_status, $post ) { + if ( ! $post instanceof \WP_Post || self::$post_type !== $post->post_type ) { + return; + } + + // Only stamp genuinely new events (leaving auto-draft/new); legacy re-saves transition from publish/draft and must stay unstamped. + if ( ! in_array( $old_status, array( 'new', 'auto-draft' ), true ) ) { + return; + } + + // Never overwrite an existing version (e.g. a draft re-published). + if ( '' !== get_post_meta( $post->ID, 'se_event_version', true ) ) { + return; + } + + update_post_meta( $post->ID, 'se_event_version', SE_MIGRATION_VERSION ); + } + /** * Deletes event dates if no event info block is present. * diff --git a/tests/e2e/.env.example b/tests/e2e/.env.example new file mode 100644 index 0000000..23c1a04 --- /dev/null +++ b/tests/e2e/.env.example @@ -0,0 +1,5 @@ +# Copy to .env and adjust for your local environment. +# Defaults match `npx wp-env start` out of the box. +WP_BASE_URL=http://localhost:8888 +WP_USERNAME=admin +WP_PASSWORD=password diff --git a/tests/e2e/fixtures/event-editor.js b/tests/e2e/fixtures/event-editor.js new file mode 100644 index 0000000..f7d4aea --- /dev/null +++ b/tests/e2e/fixtures/event-editor.js @@ -0,0 +1,212 @@ +/** + * Helpers for interacting with the Simple Events `se-event` editor in Playwright. + * + * Selectors target stable Gutenberg landmarks and the `simple-events/event-info` + * block's own class names (.se-add-date-button, .se__button-done, etc.). + */ + +const NEW_EVENT_PATH = '/wp-admin/post-new.php?post_type=se-event'; + +/** + * Open a fresh autodraft `se-event` editor and wait until the event-info block + * has been auto-inserted and the dateManager initialised. + * + * @param {import('@playwright/test').Page} page + */ +async function openNewEvent( page ) { + await page.goto( NEW_EVENT_PATH ); + + // Wait for the editor to finish booting. + await page.waitForFunction( () => + window.wp && + window.wp.data && + window.wp.data.select( 'core/editor' ) && + window.wp.data.select( 'core/editor' ).getCurrentPostId() > 0 + ); + + // Dismiss the Gutenberg welcome guide modal — it intercepts clicks on the + // event-info block. Setting the preference is more reliable than clicking + // a close button across Gutenberg versions. + await page.evaluate( () => { + const prefs = window.wp.data.dispatch( 'core/preferences' ); + if ( prefs && prefs.set ) { + prefs.set( 'core/edit-post', 'welcomeGuide', false ); + prefs.set( 'core', 'welcomeGuide', false ); + prefs.set( 'core/edit-post', 'welcomeGuideTemplate', false ); + } + } ); + + // If the modal is already rendered, wait for it to detach. + await page.locator( '.components-modal__screen-overlay' ) + .waitFor( { state: 'detached', timeout: 5000 } ) + .catch( () => {} ); + + // Wait for the auto-inserted event-info block. + await page.waitForFunction( () => { + const blocks = window.wp.data.select( 'core/block-editor' ).getBlocks(); + return blocks && blocks.some( ( b ) => b.name === 'simple-events/event-info' ); + } ); +} + +/** + * Set the post title via wp.data. UI-driven title setting is fragile across + * Gutenberg versions (the title textbox often has no accessible name), so we + * dispatch the edit directly. Title doesn't matter for the bug repro — only + * dirty state and child-date persistence do. + * + * @param {import('@playwright/test').Page} page + * @param {string} title + */ +async function setTitle( page, title ) { + await page.evaluate( ( t ) => { + window.wp.data.dispatch( 'core/editor' ).editPost( { title: t } ); + }, title ); +} + +/** + * Click the "Add Date" button inside the event-info block. + * + * @param {import('@playwright/test').Page} page + */ +async function clickAddDate( page ) { + await page.locator( '.se-add-date-button' ).click(); +} + +/** + * Click the "Done" button inside the event-info block (exits edit mode). + * + * @param {import('@playwright/test').Page} page + */ +async function clickDone( page ) { + await page.locator( '.se__button-done' ).click(); +} + +/** + * Click Publish + confirm Publish (Gutenberg's two-step publish flow). + * + * @param {import('@playwright/test').Page} page + */ +async function publish( page ) { + // First click opens the publish sidebar. + await page.getByRole( 'button', { name: /^publish$/i } ).first().click(); + // Wait for the panel to render — name-then-confirm flow. + await page.waitForTimeout( 500 ); + // Second click confirms — there should now be a second Publish button in the panel. + const confirmPublish = page.getByRole( 'button', { name: /^publish$/i } ).nth( 1 ); + if ( await confirmPublish.isVisible().catch( () => false ) ) { + await confirmPublish.click(); + } +} + +/** + * Wait until `isSavingPost()` has been false for `quietMs` continuous milliseconds. + * Catches the case where the plugin's subscribe handler triggers a second savePost() + * after the first one completes. + * + * @param {import('@playwright/test').Page} page + * @param {number} quietMs + * @param {number} timeoutMs + */ +async function waitForSaveQuiet( page, quietMs = 3000, timeoutMs = 30000 ) { + const start = Date.now(); + let lastBusyAt = Date.now(); + while ( Date.now() - start < timeoutMs ) { + const busy = await page.evaluate( () => { + const editor = window.wp.data.select( 'core/editor' ); + return editor.isSavingPost() || editor.isAutosavingPost(); + } ); + if ( busy ) { + lastBusyAt = Date.now(); + } else if ( Date.now() - lastBusyAt >= quietMs ) { + return; + } + await page.waitForTimeout( 250 ); + } + throw new Error( `Editor never settled (>${ timeoutMs }ms with no ${ quietMs }ms quiet window)` ); +} + +/** + * Read selected editor state via wp.data. + * + * @param {import('@playwright/test').Page} page + */ +async function readEditorState( page ) { + return page.evaluate( () => { + const editor = window.wp.data.select( 'core/editor' ); + const blockEditor = window.wp.data.select( 'core/block-editor' ); + const eventInfo = blockEditor.getBlocks().find( ( b ) => b.name === 'simple-events/event-info' ); + return { + postId: editor.getCurrentPostId(), + status: editor.getEditedPostAttribute( 'status' ), + isDirty: editor.isEditedPostDirty(), + isSaving: editor.isSavingPost(), + isAutosaving: editor.isAutosavingPost(), + dirtyRecords: editor.__experimentalGetDirtyEntityRecords + ? editor.__experimentalGetDirtyEntityRecords() + : null, + eventDates: eventInfo ? eventInfo.attributes.eventDates : null, + }; + } ); +} + +/** + * Start a network counter that tallies relevant POSTs. + * Returns a `counts` object that mutates as requests arrive, plus a `requests` + * array of {method, url, status} entries. + * + * @param {import('@playwright/test').Page} page + */ +function startNetworkCounter( page ) { + const counts = { sync: 0, postSave: 0, autosave: 0 }; + const requests = []; + + page.on( 'request', ( req ) => { + if ( req.method() !== 'POST' ) { + return; + } + // wp-env uses plain permalinks by default → REST routes come through + // /?rest_route=%2F...%2F (URL-encoded). Decode before matching so the + // same regex works for both pretty-permalink and plain-permalink envs. + const url = decodeURIComponent( req.url() ); + if ( /\/simple-events\/event-dates\/\d+\/sync/.test( url ) ) { + counts.sync++; + requests.push( { url, kind: 'sync' } ); + } else if ( /\/wp\/v2\/se-event\/\d+\/autosaves/.test( url ) ) { + counts.autosave++; + requests.push( { url, kind: 'autosave' } ); + } else if ( /\/wp\/v2\/se-event\/\d+(?:[?&]|$)/.test( url ) ) { + counts.postSave++; + requests.push( { url, kind: 'postSave' } ); + } + } ); + + return { counts, requests }; +} + +/** + * Fetch the live list of child `se-event-date` posts for an event id via the + * plugin's REST endpoint. Uses the page's auth cookies so it works without + * setting up an application password. + * + * @param {import('@playwright/test').Page} page + * @param {number} eventId + */ +async function fetchChildDates( page, eventId ) { + return page.evaluate( async ( id ) => { + return await window.wp.apiFetch( { + path: `/simple-events/event-dates/${ id }`, + } ); + }, eventId ); +} + +module.exports = { + openNewEvent, + setTitle, + clickAddDate, + clickDone, + publish, + waitForSaveQuiet, + readEditorState, + startNetworkCounter, + fetchChildDates, +}; diff --git a/tests/e2e/fixtures/index.js b/tests/e2e/fixtures/index.js new file mode 100644 index 0000000..082dc21 --- /dev/null +++ b/tests/e2e/fixtures/index.js @@ -0,0 +1,3 @@ +module.exports = { + ...require( './event-editor' ), +}; diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js new file mode 100644 index 0000000..e4cba21 --- /dev/null +++ b/tests/e2e/global-setup.js @@ -0,0 +1,19 @@ +const { test: setup, expect } = require( '@playwright/test' ); +const fs = require( 'fs' ); +const path = require( 'path' ); + +const STORAGE_PATH = path.join( __dirname, 'artifacts/storage-states/admin.json' ); +const USERNAME = process.env.WP_USERNAME || 'admin'; +const PASSWORD = process.env.WP_PASSWORD || 'password'; + +setup( 'authenticate as admin', async ( { page, baseURL } ) => { + await page.goto( `${ baseURL }/wp-login.php` ); + await page.fill( '#user_login', USERNAME ); + await page.fill( '#user_pass', PASSWORD ); + await page.click( '#wp-submit' ); + await page.waitForURL( /wp-admin/ ); + await expect( page.locator( '#wpadminbar' ) ).toBeVisible(); + + fs.mkdirSync( path.dirname( STORAGE_PATH ), { recursive: true } ); + await page.context().storageState( { path: STORAGE_PATH } ); +} ); diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js new file mode 100644 index 0000000..0198735 --- /dev/null +++ b/tests/e2e/playwright.config.js @@ -0,0 +1,45 @@ +const { defineConfig, devices } = require( '@playwright/test' ); +const path = require( 'path' ); +require( 'dotenv' ).config( { path: path.join( __dirname, '.env' ) } ); + +const BASE_URL = process.env.WP_BASE_URL || 'http://localhost:8888'; + +module.exports = defineConfig( { + testDir: __dirname, + outputDir: path.join( __dirname, '../../test-results' ), + fullyParallel: false, + forbidOnly: !! process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + timeout: 60 * 1000, + reporter: [ + [ 'html', { outputFolder: path.join( __dirname, '../../playwright-report' ), open: 'never' } ], + [ 'list' ], + ], + use: { + baseURL: BASE_URL, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + actionTimeout: 15 * 1000, + navigationTimeout: 30 * 1000, + }, + projects: [ + { + name: 'setup', + testMatch: /global-setup\.js$/, + }, + { + name: 'chromium', + testMatch: /specs\/.*\.spec\.js$/, + use: { + ...devices[ 'Desktop Chrome' ], + storageState: path.join( __dirname, 'artifacts/storage-states/admin.json' ), + launchOptions: { + slowMo: process.env.SLOWMO ? parseInt( process.env.SLOWMO, 10 ) : 0, + }, + }, + dependencies: [ 'setup' ], + }, + ], +} ); diff --git a/tests/e2e/specs/editor/event-dates-block.spec.js b/tests/e2e/specs/editor/event-dates-block.spec.js new file mode 100644 index 0000000..d8b2867 --- /dev/null +++ b/tests/e2e/specs/editor/event-dates-block.spec.js @@ -0,0 +1,47 @@ +const { test, expect } = require( '@playwright/test' ); +const { openNewEvent, readEditorState } = require( '../../fixtures' ); + +/** + * Sanity coverage for the auto-inserted event-info block. These tests should + * pass on `trunk` today — they exist to lock in the block-registration and + * post-type template contract so future changes don't accidentally break it. + */ +test.describe( 'Event Info block – sanity', () => { + test( 'auto-inserts on a new se-event', async ( { page } ) => { + await openNewEvent( page ); + + const blocks = await page.evaluate( () => + window.wp.data + .select( 'core/block-editor' ) + .getBlocks() + .map( ( b ) => b.name ) + ); + + expect( blocks ).toContain( 'simple-events/event-info' ); + } ); + + test( 'event-info block is locked against removal', async ( { page } ) => { + await openNewEvent( page ); + + const lockState = await page.evaluate( () => { + const blocks = window.wp.data.select( 'core/block-editor' ).getBlocks(); + const root = window.wp.data + .select( 'core/block-editor' ) + .getSettings(); + return { + blockCount: blocks.length, + templateLock: root.templateLock || null, + }; + } ); + + expect( lockState.blockCount ).toBeGreaterThan( 0 ); + } ); + + test( 'autodraft has a real post id before any save', async ( { page } ) => { + await openNewEvent( page ); + const state = await readEditorState( page ); + + expect( state.postId ).toBeGreaterThan( 0 ); + expect( state.status ).toBe( 'auto-draft' ); + } ); +} ); diff --git a/tests/e2e/specs/editor/event-dates-regressions.spec.js b/tests/e2e/specs/editor/event-dates-regressions.spec.js new file mode 100644 index 0000000..abd6b1e --- /dev/null +++ b/tests/e2e/specs/editor/event-dates-regressions.spec.js @@ -0,0 +1,116 @@ +const { test, expect } = require( '@playwright/test' ); +const { + openNewEvent, + setTitle, + clickAddDate, + clickDone, + publish, + waitForSaveQuiet, + readEditorState, + startNetworkCounter, + fetchChildDates, +} = require( '../../fixtures' ); + +/** + * Regression coverage around the event-dates save flow. All specs here run + * and must stay green. + */ + +test.describe( 'Event dates – regression coverage', () => { + /** + * Edit-and-save on an already-published, reloaded post must stay clean + * (no /sync, one post PUT, no dirty leak). + */ + test( 'edit-and-save on a reloaded published post stays clean', async ( { page } ) => { + const net = startNetworkCounter( page ); + + await openNewEvent( page ); + await setTitle( page, 'E2E Scenario C Repro' ); + await clickAddDate( page ); + await clickDone( page ); + await publish( page ); + await waitForSaveQuiet( page, 4000, 30000 ); + + const { postId } = await readEditorState( page ); + expect( postId ).toBeGreaterThan( 0 ); + + // Hard reload — drops the in-memory dirty leak. + await page.goto( `/wp-admin/post.php?post=${ postId }&action=edit` ); + await page.waitForFunction( () => + window.wp.data && + window.wp.data.select( 'core/block-editor' ) + .getBlocks() + .some( ( b ) => b.name === 'simple-events/event-info' ) + ); + + // Confirm reload itself leaves the post clean (this is the load-time + // part of the dirty-leak that the fix addresses). + const onReload = await readEditorState( page ); + expect( onReload.isDirty, 'post should be clean immediately after reload' ).toBe( false ); + + // Reset counters for the edit phase only. + net.counts.sync = 0; + net.counts.postSave = 0; + net.counts.autosave = 0; + net.requests.length = 0; + + // Make a non-date edit (title change) so the Save button is enabled, + // then save. Since the dateManager isn't dirty, the click interceptor + // in the block does NOT fire /sync — we should see exactly one post + // PUT and zero /sync calls. + await setTitle( page, 'E2E Scenario C Repro — edited' ); + await page.getByRole( 'button', { name: /^(update|save)$/i } ).first().click(); + await waitForSaveQuiet( page, 4000, 30000 ); + + const state = await readEditorState( page ); + expect( state.isDirty ).toBe( false ); + expect( net.counts.sync ).toBe( 0 ); + expect( net.counts.postSave ).toBe( 1 ); + } ); + + /** + * Revert Changes must not fire a /sync — there's nothing to send. + * Works today and must keep working after the fix. + */ + test( 'Revert Changes does not fire a /sync', async ( { page } ) => { + const net = startNetworkCounter( page ); + + await openNewEvent( page ); + await setTitle( page, 'E2E Revert Test' ); + await clickAddDate( page ); + + const revertButton = page.locator( '.se-revert-changes-button' ); + await expect( revertButton ).toBeEnabled(); + await revertButton.click(); + + // Brief settle to allow any inadvertent debounce to fire. + await page.waitForTimeout( 1500 ); + + expect( net.counts.sync ).toBe( 0 ); + } ); + + /** + * Multi-date first publish: one /sync, one child per date, no dirty leak. + */ + test( 'three dates on first publish create exactly three children, one sync', async ( { page } ) => { + const net = startNetworkCounter( page ); + + await openNewEvent( page ); + await setTitle( page, 'E2E Multi-date Publish' ); + + await clickAddDate( page ); + await clickAddDate( page ); + await clickAddDate( page ); + await clickDone( page ); + + await publish( page ); + await waitForSaveQuiet( page, 4000, 30000 ); + + const state = await readEditorState( page ); + const children = await fetchChildDates( page, state.postId ); + + expect( state.isDirty ).toBe( false ); + expect( net.counts.sync ).toBe( 1 ); + expect( children.dates ).toHaveLength( 3 ); + } ); +} ); diff --git a/tests/e2e/specs/editor/event-dates-reload.spec.js b/tests/e2e/specs/editor/event-dates-reload.spec.js new file mode 100644 index 0000000..dd33ef1 --- /dev/null +++ b/tests/e2e/specs/editor/event-dates-reload.spec.js @@ -0,0 +1,103 @@ +const { test, expect } = require( '@playwright/test' ); +const { + openNewEvent, + setTitle, + clickAddDate, + clickDone, + publish, + waitForSaveQuiet, + readEditorState, + startNetworkCounter, +} = require( '../../fixtures' ); + +/** + * User's manual repro path: + * 1. Open new event. + * 2. Add a date via the GUI (Add Date button). + * 3. Save (Publish). + * 4. Wait for the save + child-date creation to finish. + * 5. Save again. + * 6. Reload the page. + * + * After step 6, the browser should NOT show a "Leave site? Changes you made + * may not be saved" dialog. That dialog is driven by `isEditedPostDirty()` + * being true at unload time. + */ +test( 'publish → save again → reload: no unsaved-changes prompt fires', async ( { page } ) => { + // Capture every dialog that fires during the run. + const dialogs = []; + page.on( 'dialog', async ( dialog ) => { + dialogs.push( { type: dialog.type(), message: dialog.message() } ); + console.log( `[dialog] type=${ dialog.type() } message="${ dialog.message() }"` ); + await dialog.accept(); + } ); + + const net = startNetworkCounter( page ); + + console.log( '— Step 1: open new event' ); + await openNewEvent( page ); + await setTitle( page, 'E2E Reload Repro' ); + + console.log( '— Step 2: add a date via GUI' ); + await clickAddDate( page ); + await clickDone( page ); + + console.log( '— Step 3: publish (first save)' ); + await publish( page ); + + console.log( '— Step 4: wait for snackbar + save to settle' ); + await expect( + page.getByText( /Event dates synced/i ).first() + ).toBeVisible( { timeout: 10000 } ); + await waitForSaveQuiet( page, 4000, 30000 ); + + const afterFirst = await readEditorState( page ); + console.log( 'After first save:', { + isDirty: afterFirst.isDirty, + status: afterFirst.status, + counts: { ...net.counts }, + } ); + + console.log( '— Step 5: save again' ); + const saveBtn = page.getByRole( 'button', { name: /^(update|save)$/i } ).first(); + const disabledAttr = await saveBtn.getAttribute( 'aria-disabled' ); + if ( disabledAttr === 'true' ) { + console.log( + ' Save button is aria-disabled — wp-env env reports post is clean.\n' + + ' Falling back to programmatic savePost() so the flow still exercises\n' + + ' the subscribe path.' + ); + await page.evaluate( () => window.wp.data.dispatch( 'core/editor' ).savePost() ); + } else { + console.log( ' Save button is enabled — clicking it.' ); + await saveBtn.click(); + } + await waitForSaveQuiet( page, 4000, 30000 ); + + const afterSecond = await readEditorState( page ); + console.log( 'After second save:', { + isDirty: afterSecond.isDirty, + counts: { ...net.counts }, + } ); + + console.log( '— Step 6: reload the page' ); + await page.reload(); + await page.waitForFunction( () => + window.wp && + window.wp.data && + window.wp.data.select( 'core/editor' ) && + window.wp.data.select( 'core/editor' ).getCurrentPostId() > 0 + ); + + const afterReload = await readEditorState( page ); + console.log( 'After reload:', { + isDirty: afterReload.isDirty, + status: afterReload.status, + } ); + console.log( 'Dialogs captured during run:', dialogs ); + + // Assertions + const beforeUnload = dialogs.find( ( d ) => d.type === 'beforeunload' ); + expect( beforeUnload, '"Leave site?" beforeunload should NOT fire' ).toBeUndefined(); + expect( afterReload.isDirty, 'post should be clean after reload' ).toBe( false ); +} ); diff --git a/tests/e2e/specs/editor/event-dates-save.spec.js b/tests/e2e/specs/editor/event-dates-save.spec.js new file mode 100644 index 0000000..210b0e1 --- /dev/null +++ b/tests/e2e/specs/editor/event-dates-save.spec.js @@ -0,0 +1,107 @@ +const { test, expect } = require( '@playwright/test' ); +const { + openNewEvent, + setTitle, + clickAddDate, + clickDone, + publish, + waitForSaveQuiet, + readEditorState, + startNetworkCounter, + fetchChildDates, +} = require( '../../fixtures' ); + +/** + * Primary repro for the autodraft → publish bug observed via the chrome-tool + * agent. On `trunk` today this test FAILS — that's intentional. It documents + * the bug. Once the fix lands the assertions flip to green. + * + * Three things must be true after a clean first-publish: + * 1. Exactly one POST to /simple-events/event-dates/{id}/sync. + * 2. isEditedPostDirty() === false after the save settles. + * 3. Exactly one child se-event-date post in the DB (read via REST). + * + * Today: sync fires twice (server create→delete→create), so the child date + * id churns and Gutenberg sees a late setAttributes diff → stuck dirty. + */ +test.describe( 'Event dates – first publish from autodraft', () => { + test( 'leaves the post clean and produces exactly one child date', async ( { page } ) => { + const consoleErrors = []; + page.on( 'console', ( msg ) => { + if ( msg.type() === 'error' ) { + consoleErrors.push( msg.text() ); + } + } ); + + const net = startNetworkCounter( page ); + + await openNewEvent( page ); + await setTitle( page, 'E2E First Publish Repro' ); + + // Pre-Add Date sanity. + const before = await readEditorState( page ); + expect( before.status ).toBe( 'auto-draft' ); + + await clickAddDate( page ); + await clickDone( page ); + + // The yellow in-block banner should appear (dateManager.isDirty=true). + await expect( page.locator( '.se-unsaved-changes-message' ) ).toBeVisible(); + + await publish( page ); + + // Wait for the post-save snackbar. + await expect( + page.getByText( /Event dates synced/i ).first() + ).toBeVisible( { timeout: 10000 } ); + await waitForSaveQuiet( page, 4000, 30000 ); + + const after = await readEditorState( page ); + const children = await fetchChildDates( page, after.postId ); + + console.log( 'AFTER PUBLISH:', JSON.stringify( after, null, 2 ) ); + console.log( 'NETWORK COUNTS:', net.counts ); + console.log( + 'REQUESTS:', + net.requests.map( ( r ) => `${ r.kind } ${ r.url }` ) + ); + console.log( 'CHILD DATES VIA REST:', JSON.stringify( children, null, 2 ) ); + + // === Assertions that should pass after the fix === + + // (1) Exactly one /sync POST per first publish. Fails today (3× in + // local wp-env). + expect( net.counts.sync ).toBe( 1 ); + + // (2) Exactly one post PUT per first publish. Fails today (2× in + // local wp-env — Gutenberg fires a follow-up after the subscribe's + // dateSavePromise.then(savePost) chain). + expect( net.counts.postSave ).toBe( 1 ); + + // (3) Exactly one child se-event-date in the DB. + expect( children.dates ).toHaveLength( 1 ); + + // (4) No console errors. + expect( consoleErrors ).toEqual( [] ); + + // (5) The yellow in-block banner is gone (dateManager.isDirty cleared + // by refreshWithNewDates after /sync response). + await expect( page.locator( '.se-unsaved-changes-message' ) ).toBeHidden(); + + // Note on Gutenberg's `isEditedPostDirty()`: + // The chrome-agent run (on a hosted WP env) saw this stuck at `true` + // after first publish. Local wp-env does NOT reproduce that symptom — + // the Save button greys out, the post is genuinely clean. The fix + // should clear the leak in any env that exhibits it, but asserting on + // it here would be flaky. Re-test against your devilbox env if you + // want to confirm the dirty leak is gone post-fix. + // expect( after.isDirty ).toBe( false ); + } ); +} ); + +// TODO follow-up specs once the primary repro is green: +// - "Clicking Update a second time on a stuck-dirty post clears dirty" (Scenario B regression) +// - "Edit-and-save on reloaded published event stays clean" (Scenario C — regression-prevention) +// - "Save while /sync is in-flight is locked" (Approach A only — needs lockPostSaving in place) +// - "Revert Changes fires no /sync" +// - "Multi-date first publish creates N children, not 2N" diff --git a/tests/phpunit/EventDatesCleanupTest.php b/tests/phpunit/EventDates/EventDatesCleanupTest.php similarity index 100% rename from tests/phpunit/EventDatesCleanupTest.php rename to tests/phpunit/EventDates/EventDatesCleanupTest.php diff --git a/tests/phpunit/Migration/EventMigrationTest.php b/tests/phpunit/Migration/EventMigrationTest.php new file mode 100644 index 0000000..8ca531c --- /dev/null +++ b/tests/phpunit/Migration/EventMigrationTest.php @@ -0,0 +1,122 @@ + + */ + private function migration_ids(): array { + return array_map( + static function ( $post ) { + return (int) $post->ID; + }, + SE_Migrate_Events::get_events_to_migrate() + ); + } + + /** + * A new event created directly as published must not be flagged for + * migration — even though no /sync ever ran. + * + * @testdox When a new event is created and published, the current version meta is stamped automatically on creation, so the migration check never flags it even though no date sync ever ran + * + * @return void + */ + public function test_new_published_event_is_not_flagged_for_migration() { + $event_id = $this->factory->post->create( + array( + 'post_type' => 'se-event', + 'post_status' => 'publish', + ) + ); + + $this->assertNotContains( + $event_id, + $this->migration_ids(), + 'A freshly created event should not appear in the migration queue.' + ); + } + + /** + * The real Gutenberg flow: an event starts as an auto-draft and is then + * published. Once published it must not be flagged for migration. + * + * @testdox When an event moves from auto-draft to published (the normal Gutenberg editor flow), the version meta is stamped on that status transition, so the event is never wrongly flagged for migration + * + * @return void + */ + public function test_autodraft_event_published_is_not_flagged() { + $event_id = $this->factory->post->create( + array( + 'post_type' => 'se-event', + 'post_status' => 'auto-draft', + ) + ); + + wp_update_post( + array( + 'ID' => $event_id, + 'post_status' => 'publish', + ) + ); + + $this->assertNotContains( + $event_id, + $this->migration_ids(), + 'An event published from auto-draft should not be flagged for migration.' + ); + $this->assertFalse( + SE_Migrate_Events::has_events_to_migrate(), + 'No events should require migration when only a fresh event exists.' + ); + } + + /** + * Regression guard: the new-event behaviour must NOT mask a genuinely + * legacy event. An existing published event whose version meta is absent + * (and which is only re-saved, never transitioning from auto-draft/new) + * must still be flagged for migration. + * + * @testdox A genuinely legacy event with no version meta that is only re-saved (never transitioning from auto-draft) must still be detected by the migration system, so the new-event stamping does not accidentally skip real migrations + * + * @return void + */ + public function test_legacy_event_without_version_is_still_flagged() { + $event_id = $this->factory->post->create( + array( + 'post_type' => 'se-event', + 'post_status' => 'publish', + ) + ); + + // Simulate a pre-2.0.0 event: no version meta, and it is not + // transitioning out of auto-draft/new (just a normal re-save). + delete_post_meta( $event_id, 'se_event_version' ); + wp_update_post( + array( + 'ID' => $event_id, + 'post_title' => 'Edited legacy event', + ) + ); + + $this->assertContains( + $event_id, + $this->migration_ids(), + 'A legacy event with no version meta must still be flagged for migration.' + ); + } +} diff --git a/tests/phpunit/EventQueryUtilsTest.php b/tests/phpunit/Query/EventQueryUtilsTest.php similarity index 100% rename from tests/phpunit/EventQueryUtilsTest.php rename to tests/phpunit/Query/EventQueryUtilsTest.php diff --git a/tests/phpunit/SampleTest.php b/tests/phpunit/Smoke/SampleTest.php similarity index 100% rename from tests/phpunit/SampleTest.php rename to tests/phpunit/Smoke/SampleTest.php diff --git a/tests/phpunit/TemplateFunctionsTest.php b/tests/phpunit/Templates/TemplateFunctionsTest.php similarity index 100% rename from tests/phpunit/TemplateFunctionsTest.php rename to tests/phpunit/Templates/TemplateFunctionsTest.php