diff --git a/composer.json b/composer.json index 560909d..dd1f062 100644 --- a/composer.json +++ b/composer.json @@ -37,26 +37,37 @@ }, "require-dev": { "automattic/vipwpcs": "^3.0", + "brain/monkey": "^2.6", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "phpcompatibility/phpcompatibility-wp": "^3.0.0-alpha", "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^2.1.22", "phpstan/phpstan-deprecation-rules": "^2.0.3", "phpstan/phpstan-phpunit": "^2.0.3", - "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^11.0", "slevomat/coding-standard": "^8.0", "squizlabs/php_codesniffer": "^3.9", "szepeviktor/phpstan-wordpress": "^2.0.2", "wp-coding-standards/wpcs": "^3.1", - "wpackagist-plugin/plugin-check": "~1.6.0" + "wpackagist-plugin/plugin-check": "~1.6.0", + "yoast/phpunit-polyfills": "^3.0" }, "autoload": { "psr-4": { "Core_Carousel\\": "inc/" } }, + "autoload-dev": { + "psr-4": { + "Core_Carousel\\Tests\\": "tests/php/" + } + }, "scripts": { "format": "phpcbf", "lint": "phpcs", - "phpstan": "phpstan analyse --memory-limit=2G" + "phpstan": "phpstan analyse --memory-limit=2G", + "test": "phpunit", + "test:unit": "phpunit --testsuite unit", + "test:coverage": "phpunit --coverage-html tests/_output/coverage" } } diff --git a/composer.lock b/composer.lock index d486c94..719a5fd 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": "074cbb3002be39e767760242c625626e", + "content-hash": "c807d59c2478e04aa04acd36971c41e8", "packages": [ { "name": "scrivo/highlight.php", @@ -86,6 +86,54 @@ } ], "packages-dev": [ + { + "name": "antecedent/patchwork", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/antecedent/patchwork.git", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignas Rudaitis", + "email": "ignas.rudaitis@gmail.com" + } + ], + "description": "Method redefinition (monkey-patching) functionality for PHP.", + "homepage": "https://antecedent.github.io/patchwork/", + "keywords": [ + "aop", + "aspect", + "interception", + "monkeypatching", + "redefinition", + "runkit", + "testing" + ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.2.3" + }, + "time": "2025-09-17T09:00:56+00:00" + }, { "name": "automattic/vipwpcs", "version": "3.0.1", @@ -140,6 +188,76 @@ }, "time": "2024-05-10T20:31:09+00:00" }, + { + "name": "brain/monkey", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/Brain-WP/BrainMonkey.git", + "reference": "ea3aeb3d559ba3c0930b3f4d210b665a4c044d83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/ea3aeb3d559ba3c0930b3f4d210b665a4c044d83", + "reference": "ea3aeb3d559ba3c0930b3f4d210b665a4c044d83", + "shasum": "" + }, + "require": { + "antecedent/patchwork": "^2.1.17", + "mockery/mockery": "~1.3.6 || ~1.4.4 || ~1.5.1 || ^1.6.10", + "php": ">=5.6.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "phpcompatibility/php-compatibility": "^9.3.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.49 || ^9.6.30" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev", + "dev-version/1": "1.x-dev" + } + }, + "autoload": { + "files": [ + "inc/api.php" + ], + "psr-4": { + "Brain\\Monkey\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Giuseppe Mazzapica", + "email": "giuseppe.mazzapica@gmail.com", + "homepage": "https://gmazzap.me", + "role": "Developer" + } + ], + "description": "Mocking utility for PHP functions and WordPress plugin API", + "keywords": [ + "Monkey Patching", + "interception", + "mock", + "mock functions", + "mockery", + "patchwork", + "redefinition", + "runkit", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/Brain-WP/BrainMonkey/issues", + "source": "https://github.com/Brain-WP/BrainMonkey" + }, + "time": "2026-02-05T09:22:14+00:00" + }, { "name": "composer/installers", "version": "v2.3.0", @@ -383,706 +501,2520 @@ "time": "2025-11-11T04:32:07+00:00" }, { - "name": "php-stubs/wordpress-stubs", - "version": "v6.9.0", + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", "source": { "type": "git", - "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a" + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", - "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, - "conflict": { - "phpdocumentor/reflection-docblock": "5.6.1" + "require": { + "php": "^7.4|^8.0" }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^5.5", - "php": "^7.4 || ^8.0", - "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.4.1", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^9.5", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", - "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" }, - "suggest": { - "paragonie/sodium_compat": "Pure PHP implementation of libsodium", - "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "WordPress function and class declaration stubs for static analysis.", - "homepage": "https://github.com/php-stubs/wordpress-stubs", + "description": "This is the PHP port of Hamcrest Matchers", "keywords": [ - "PHPStan", - "static analysis", - "wordpress" + "test" ], "support": { - "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.0" + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2025-12-03T23:06:24+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { - "name": "phpcompatibility/php-compatibility", - "version": "10.0.0-alpha2", + "name": "mockery/mockery", + "version": "1.6.12", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18" + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", - "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { - "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.1.2", - "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" }, - "replace": { - "wimg/php-compatibility": "*" + "conflict": { + "phpunit/phpunit": "<8.0" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.2.0", - "phpcsstandards/phpcsdevtools": "^1.2.3", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.5.32 || ^11.3.3", - "yoast/phpunit-polyfills": "^1.1.5 || ^2.0.5 || ^3.1.0" + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "9.x-dev", - "dev-develop": "10.x-dev" + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" }, { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" } ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "https://techblog.wimgodden.be/tag/codesniffer/", + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", "keywords": [ - "compatibility", - "phpcs", - "standards", - "static analysis" + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "security": "https://github.com/PHPCompatibility/PHPCompatibility/security/policy", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" }, - "funding": [ - { - "url": "https://github.com/PHPCompatibility", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcompatibility", - "type": "thanks_dev" - } - ], - "time": "2025-11-28T11:36:33+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { - "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "2.0.0-alpha2", + "name": "myclabs/deep-copy", + "version": "1.13.4", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/7a979711c87d8202b52f56c56bd719d09d8ed7f5", - "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^10.0@dev" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "paragonie/random_compat": "dev-master", - "paragonie/sodium_compat": "dev-master" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Wim Godden", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "lead" - } + "MIT" ], - "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", - "homepage": "http://phpcompatibility.com/", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "compatibility", - "paragonie", - "phpcs", - "polyfill", - "standards", - "static analysis" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", - "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { - "url": "https://github.com/PHPCompatibility", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcompatibility", - "type": "thanks_dev" + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" } ], - "time": "2025-11-29T13:09:49+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { - "name": "phpcompatibility/phpcompatibility-wp", - "version": "3.0.0-alpha2", + "name": "nikic/php-parser", + "version": "v5.7.0", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", - "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^10.0@dev", - "phpcompatibility/phpcompatibility-paragonie": "^2.0@dev" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "lead" + "name": "Nikita Popov" } ], - "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.1", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "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" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" + }, + "time": "2026-02-03T19:29:21+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "10.0.0-alpha2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", + "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" + }, + "replace": { + "wimg/php-compatibility": "*" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.3", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.5.32 || ^11.3.3", + "yoast/phpunit-polyfills": "^1.1.5 || ^2.0.5 || ^3.1.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev", + "dev-develop": "10.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "https://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibility/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-11-28T11:36:33+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "2.0.0-alpha2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/7a979711c87d8202b52f56c56bd719d09d8ed7f5", + "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^10.0@dev" + }, + "require-dev": { + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", "homepage": "http://phpcompatibility.com/", "keywords": [ - "compatibility", - "phpcs", - "standards", - "static analysis", - "wordpress" + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-11-29T13:09:49+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "3.0.0-alpha2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", + "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^10.0@dev", + "phpcompatibility/phpcompatibility-paragonie": "^2.0@dev" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-12-16T13:35:20+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.38", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-01-30T17:12:46+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.15" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" + }, + "time": "2025-05-14T10:56:57+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "e4c5a22bf43d3d2bd5a780ad261a622ff62c49a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/e4c5a22bf43d3d2bd5a780ad261a622ff62c49a4", + "reference": "e4c5a22bf43d3d2bd5a780ad261a622ff62c49a4", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.32" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "nikic/php-parser": "^5", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.12" + }, + "time": "2026-01-22T13:40:00+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.51", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ad14159f92910b0f0e3928c13e9b2077529de091" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091", + "reference": "ad14159f92910b0f0e3928c13e9b2077529de091", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-05T07:59:30+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", - "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { - "url": "https://github.com/PHPCompatibility", + "url": "https://github.com/sebastianbergmann", "type": "github" }, { - "url": "https://github.com/jrfnl", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://thanks.dev/u/gh/phpcompatibility", - "type": "thanks_dev" + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2025-12-16T13:35:20+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { - "name": "phpcsstandards/phpcsextra", - "version": "1.5.0", + "name": "sebastian/exporter", + "version": "6.3.2", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "b598aa890815b8df16363271b659d73280129101" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", - "reference": "b598aa890815b8df16363271b659d73280129101", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { - "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.2.0", - "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.2.0", - "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^11.3" }, - "type": "phpcodesniffer-standard", + "type": "library", "extra": { "branch-alias": { - "dev-stable": "1.x-dev", - "dev-develop": "1.x-dev" + "dev-main": "6.3-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ - "PHP_CodeSniffer", - "phpcbf", - "phpcodesniffer-standard", - "phpcs", - "standards", - "static analysis" + "export", + "exporter" ], "support": { - "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", - "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", - "source": "https://github.com/PHPCSStandards/PHPCSExtra" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", + "url": "https://github.com/sebastianbergmann", "type": "github" }, { - "url": "https://github.com/jrfnl", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2025-11-12T23:06:57+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { - "name": "phpcsstandards/phpcsutils", - "version": "1.2.2", + "name": "sebastian/global-state", + "version": "7.0.2", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", - "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "ext-filter": "*", - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.2.0", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + "ext-dom": "*", + "phpunit/phpunit": "^11.0" }, - "type": "phpcodesniffer-standard", + "type": "library", "extra": { "branch-alias": { - "dev-stable": "1.x-dev", - "dev-develop": "1.x-dev" + "dev-main": "7.0-dev" } }, "autoload": { "classmap": [ - "PHPCSUtils/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "A suite of utility functions for use with PHP_CodeSniffer", - "homepage": "https://phpcsutils.com/", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ - "PHP_CodeSniffer", - "phpcbf", - "phpcodesniffer-standard", - "phpcs", - "phpcs3", - "phpcs4", - "standards", - "static analysis", - "tokens", - "utility" + "global state" ], "support": { - "docs": "https://phpcsutils.com/", - "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", - "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", - "source": "https://github.com/PHPCSStandards/PHPCSUtils" + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2025-12-08T14:27:58+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { - "name": "phpstan/extension-installer", - "version": "1.4.3", + "name": "sebastian/object-enumerator", + "version": "6.0.1", "source": { "type": "git", - "url": "https://github.com/phpstan/extension-installer.git", - "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", - "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", "shasum": "" }, "require": { - "composer-plugin-api": "^2.0", - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0 || ^2.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "composer/composer": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + "phpunit/phpunit": "^11.0" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PHPStan\\ExtensionInstaller\\Plugin" + "branch-alias": { + "dev-main": "6.0-dev" + } }, "autoload": { - "psr-4": { - "PHPStan\\ExtensionInstaller\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "Composer plugin for automatic installation of PHPStan extensions", - "keywords": [ - "dev", - "static analysis" + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { - "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" }, - "time": "2024-09-04T20:21:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" }, { - "name": "phpstan/phpdoc-parser", - "version": "2.3.1", + "name": "sebastian/object-reflector", + "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=8.2" }, "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" + "phpunit/phpunit": "^11.0" }, "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, - "time": "2026-01-12T11:33:04+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" }, { - "name": "phpstan/phpstan", - "version": "2.1.33", + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", - "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", "shasum": "" }, "require": { - "php": "^7.4|^8.0" + "php": ">=8.2" }, - "conflict": { - "phpstan/phpstan-shim": "*" + "require-dev": { + "phpunit/phpunit": "^11.3" }, - "bin": [ - "phpstan", - "phpstan.phar" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, "autoload": { - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", + "url": "https://github.com/sebastianbergmann", "type": "github" }, { - "url": "https://github.com/phpstan", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2025-12-05T10:24:31+00:00" + "time": "2025-08-13T04:42:22+00:00" }, { - "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.3", + "name": "sebastian/type", + "version": "5.1.3", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "468e02c9176891cc901143da118f09dc9505fc2f" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", - "reference": "468e02c9176891cc901143da118f09dc9505fc2f", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.15" + "php": ">=8.2" }, "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^11.3" }, - "type": "phpstan-extension", + "type": "library", "extra": { - "phpstan": { - "includes": [ - "rules.neon" - ] + "branch-alias": { + "dev-main": "5.1-dev" } }, "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, - "time": "2025-05-14T10:56:57+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" }, { - "name": "phpstan/phpstan-phpunit", - "version": "2.0.11", + "name": "sebastian/version", + "version": "5.0.2", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "5e30669bef866eff70db8b58d72a5c185aa82414" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/5e30669bef866eff70db8b58d72a5c185aa82414", - "reference": "5e30669bef866eff70db8b58d72a5c185aa82414", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.32" - }, - "conflict": { - "phpunit/phpunit": "<7.0" - }, - "require-dev": { - "nikic/php-parser": "^5", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6" + "php": ">=8.2" }, - "type": "phpstan-extension", + "type": "library", "extra": { - "phpstan": { - "includes": [ - "extension.neon", - "rules.neon" - ] + "branch-alias": { + "dev-main": "5.0-dev" } }, "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPUnit extensions and rules for PHPStan", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.11" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" }, - "time": "2025-12-19T09:05:35+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" }, { "name": "sirbrillig/phpcs-variable-analysis", @@ -1284,6 +3216,58 @@ ], "time": "2025-11-04T16:30:35+00:00" }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, { "name": "szepeviktor/phpstan-wordpress", "version": "v2.0.3", @@ -1347,6 +3331,56 @@ }, "time": "2025-09-14T02:58:22+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.3.0", @@ -1430,6 +3464,69 @@ }, "type": "wordpress-plugin", "homepage": "https://wordpress.org/plugins/plugin-check/" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "9cf2ccd990eadfc4a1e390592d4731e590b2c618" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/9cf2ccd990eadfc4a1e390592d4731e590b2c618", + "reference": "9cf2ccd990eadfc4a1e390592d4731e590b2c618", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "phpunit/phpunit": "^6.4.4 || ^7.0 || ^8.0 || ^9.0 || ^11.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2025-02-09T18:36:24+00:00" } ], "aliases": [], diff --git a/core-carousel.php b/core-carousel.php index 6daaf49..5c177ee 100644 --- a/core-carousel.php +++ b/core-carousel.php @@ -2,7 +2,9 @@ /** * Plugin Name: Core Carousel * Description: Carousel block using Embla and WordPress Interactivity API. - * Plugin URI: https://rtcamp.com + * Plugin URI: https://github.com/rtCamp/core-carousel + * Requires at least: 6.1 + * Requires PHP: 7.4 * Author: rtCamp * Author URI: https://rtcamp.com * License: GPL2 diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..9c127e7 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,76 @@ +/** + * Jest configuration for Core Carousel plugin. + * + * Extends @wordpress/scripts default configuration with: + * - Custom test setup for WordPress and Embla mocks + * - Module path aliases for cleaner imports + * - Coverage thresholds to maintain code quality + * + * @see https://jestjs.io/docs/configuration + */ + +const defaultConfig = require( '@wordpress/scripts/config/jest-unit.config' ); + +module.exports = { + ...defaultConfig, + + // Display name for clarity in multi-project setups + displayName: 'core-carousel', + + // Test setup files run after Jest environment is set up + setupFilesAfterEnv: [ + ...( defaultConfig.setupFilesAfterEnv || [] ), + '/tests/js/setup.ts', + ], + + // Module resolution aliases + moduleNameMapper: { + ...defaultConfig.moduleNameMapper, + // Path alias for src directory + '^@/(.*)$': '/src/$1', + // Mock for WordPress Interactivity API (not available in test environment) + '^@wordpress/interactivity$': '/tests/js/__mocks__/wordpress-interactivity.ts', + }, + + // Directories to ignore when searching for tests + testPathIgnorePatterns: [ + '/node_modules/', + '/build/', + '/vendor/', + ], + + // Files to include in coverage reports + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + // Exclude type definition files + '!src/**/*.d.ts', + // Exclude barrel exports + '!src/**/index.ts', + // Exclude WordPress block JSON schemas + '!src/**/*.json', + ], + + // Coverage thresholds - fail if coverage drops below these percentages + coverageThreshold: { + global: { + branches: 40, + functions: 40, + lines: 40, + statements: 40, + }, + }, + + // Coverage reporters for different outputs + coverageReporters: [ + 'text', + 'text-summary', + 'lcov', + 'html', + ], + + // Verbose output for CI environments + verbose: process.env.CI === 'true', + + // Timeout for slow tests (useful for integration tests) + testTimeout: 10000, +}; diff --git a/package-lock.json b/package-lock.json index e8501f8..b4313f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,9 @@ "devDependencies": { "@commitlint/cli": "20.4.1", "@commitlint/config-conventional": "20.4.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@types/jest": "^29.5.14", "@types/react": "^18.3.27", "@types/react-dom": "^18.3.7", "@types/wordpress__block-editor": "15.0.0", @@ -42,6 +45,13 @@ "npm": "^10.0.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -6718,6 +6728,131 @@ "integrity": "sha512-RwARl+hFwhzy0tg9atWcchLFvoQiOh4rrP7uG2N5E4W80BPCUX0ElcUR9St43fxB9EfjsW2df9Qp+UsTbvQDjA==", "license": "MIT" }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -6746,6 +6881,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -6967,6 +7110,17 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", @@ -12991,6 +13145,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -13610,6 +13771,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -19570,6 +19739,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/package.json b/package.json index d7d7e69..d933fc4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,10 @@ "lint:php:fix": "composer run-script format", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "version": "npm run changelog && git add CHANGELOG.md", - "test:actions": "act" + "test:actions": "act", + "test:js": "wp-scripts test-unit-js", + "test:js:watch": "wp-scripts test-unit-js --watch", + "test:js:coverage": "wp-scripts test-unit-js --coverage" }, "dependencies": { "@wordpress/babel-preset-default": "8.38.0", @@ -42,6 +45,9 @@ "devDependencies": { "@commitlint/cli": "20.4.1", "@commitlint/config-conventional": "20.4.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@types/jest": "^29.5.14", "@types/react": "^18.3.27", "@types/react-dom": "^18.3.7", "@types/wordpress__block-editor": "15.0.0", diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..04adb04 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests/php/Unit + + + + + inc + core-carousel.php + + + vendor + tests + + + diff --git a/src/blocks/carousel/__tests__/types.test.ts b/src/blocks/carousel/__tests__/types.test.ts new file mode 100644 index 0000000..efccbb9 --- /dev/null +++ b/src/blocks/carousel/__tests__/types.test.ts @@ -0,0 +1,570 @@ +/** + * Unit tests for the CarouselAttributes and CarouselContext type definitions. + * + * These tests verify: + * - Type structures are correct and TypeScript compilation passes + * - All valid values for union types are accepted + * - Default and edge case values work correctly + * - Optional properties behave as expected + * + * Note: These are compile-time type checks that also verify runtime behavior. + * + * @package + */ + +import type { CarouselAttributes, CarouselContext } from '../types'; + +describe( 'CarouselAttributes Type', () => { + describe( 'Structure Validation', () => { + it( 'should accept valid complete attributes', () => { + const attributes: CarouselAttributes = { + loop: true, + dragFree: false, + carouselAlign: 'center', + containScroll: 'trimSnaps', + direction: 'ltr', + axis: 'x', + height: '400px', + allowedSlideBlocks: [ 'core/image', 'core/paragraph' ], + autoplay: true, + autoplayDelay: 5000, + autoplayStopOnInteraction: true, + autoplayStopOnMouseEnter: true, + ariaLabel: 'Image carousel', + slideGap: 16, + slidesToScroll: '1', + }; + + expect( attributes ).toBeDefined(); + expect( attributes.loop ).toBe( true ); + expect( attributes.carouselAlign ).toBe( 'center' ); + expect( attributes.direction ).toBe( 'ltr' ); + } ); + + it( 'should have all required properties', () => { + const attributes: CarouselAttributes = { + loop: false, + dragFree: true, + carouselAlign: 'start', + containScroll: 'keepSnaps', + direction: 'rtl', + axis: 'y', + height: '300px', + allowedSlideBlocks: [], + autoplay: false, + autoplayDelay: 3000, + autoplayStopOnInteraction: false, + autoplayStopOnMouseEnter: false, + ariaLabel: '', + slideGap: 0, + slidesToScroll: 'auto', + }; + + // Verify all keys exist + const requiredKeys = [ + 'loop', + 'dragFree', + 'carouselAlign', + 'containScroll', + 'direction', + 'axis', + 'height', + 'allowedSlideBlocks', + 'autoplay', + 'autoplayDelay', + 'autoplayStopOnInteraction', + 'autoplayStopOnMouseEnter', + 'ariaLabel', + 'slideGap', + 'slidesToScroll', + ]; + + requiredKeys.forEach( ( key ) => { + expect( attributes ).toHaveProperty( key ); + } ); + } ); + } ); + + describe( 'Alignment Options', () => { + it.each( [ 'start', 'center', 'end' ] as const )( + 'should accept carouselAlign value: %s', + ( align ) => { + const attributes: Partial< CarouselAttributes > = { + carouselAlign: align, + }; + expect( attributes.carouselAlign ).toBe( align ); + }, + ); + + it( 'should accept optional align property', () => { + const withAlign: Partial< CarouselAttributes > = { + align: 'center', + }; + expect( withAlign.align ).toBe( 'center' ); + + const withoutAlign: Partial< CarouselAttributes > = {}; + expect( withoutAlign.align ).toBeUndefined(); + } ); + } ); + + describe( 'Scroll Containment', () => { + it.each( [ 'trimSnaps', 'keepSnaps' ] as const )( + 'should accept containScroll value: %s', + ( value ) => { + const attributes: Partial< CarouselAttributes > = { + containScroll: value, + }; + expect( attributes.containScroll ).toBe( value ); + }, + ); + } ); + + describe( 'Direction and Axis', () => { + it.each( [ 'ltr', 'rtl' ] as const )( + 'should accept direction value: %s', + ( direction ) => { + const attributes: Partial< CarouselAttributes > = { direction }; + expect( attributes.direction ).toBe( direction ); + }, + ); + + it.each( [ 'x', 'y' ] as const )( + 'should accept axis value: %s', + ( axis ) => { + const attributes: Partial< CarouselAttributes > = { axis }; + expect( attributes.axis ).toBe( axis ); + }, + ); + } ); + + describe( 'Autoplay Configuration', () => { + it( 'should accept autoplay as boolean', () => { + const enabled: Partial< CarouselAttributes > = { autoplay: true }; + const disabled: Partial< CarouselAttributes > = { autoplay: false }; + + expect( enabled.autoplay ).toBe( true ); + expect( disabled.autoplay ).toBe( false ); + } ); + + it( 'should accept valid autoplay delay values', () => { + const delays = [ 0, 1000, 3000, 5000, 10000 ]; + + delays.forEach( ( delay ) => { + const attributes: Partial< CarouselAttributes > = { + autoplayDelay: delay, + }; + expect( attributes.autoplayDelay ).toBe( delay ); + } ); + } ); + } ); + + describe( 'Slide Configuration', () => { + it( 'should accept empty allowedSlideBlocks array', () => { + const attributes: Partial< CarouselAttributes > = { + allowedSlideBlocks: [], + }; + expect( attributes.allowedSlideBlocks ).toHaveLength( 0 ); + } ); + + it( 'should accept multiple block types in allowedSlideBlocks', () => { + const blocks = [ + 'core/image', + 'core/paragraph', + 'core/heading', + 'core/group', + 'core/columns', + ]; + + const attributes: Partial< CarouselAttributes > = { + allowedSlideBlocks: blocks, + }; + + expect( attributes.allowedSlideBlocks ).toHaveLength( 5 ); + expect( attributes.allowedSlideBlocks ).toContain( 'core/image' ); + } ); + + it( 'should accept various slideGap values', () => { + const gaps = [ 0, 8, 16, 24, 32 ]; + + gaps.forEach( ( gap ) => { + const attributes: Partial< CarouselAttributes > = { + slideGap: gap, + }; + expect( attributes.slideGap ).toBe( gap ); + } ); + } ); + + it( 'should accept slidesToScroll as string', () => { + const values = [ '1', '2', '3', 'auto' ]; + + values.forEach( ( value ) => { + const attributes: Partial< CarouselAttributes > = { + slidesToScroll: value, + }; + expect( attributes.slidesToScroll ).toBe( value ); + } ); + } ); + } ); + + describe( 'Accessibility', () => { + it( 'should accept various ariaLabel values', () => { + const labels = [ + 'Image carousel', + 'Product gallery', + 'Testimonial slider', + '', + ]; + + labels.forEach( ( label ) => { + const attributes: Partial< CarouselAttributes > = { + ariaLabel: label, + }; + expect( attributes.ariaLabel ).toBe( label ); + } ); + } ); + } ); + + describe( 'Dimension Configuration', () => { + it( 'should accept various height values', () => { + const heights = [ '100px', '400px', '50vh', '100%', 'auto' ]; + + heights.forEach( ( height ) => { + const attributes: Partial< CarouselAttributes > = { height }; + expect( attributes.height ).toBe( height ); + } ); + } ); + } ); +} ); + +describe( 'CarouselContext Type', () => { + describe( 'Autoplay State', () => { + it( 'should accept context with autoplay disabled', () => { + const context: CarouselContext = { + options: { + loop: false, + align: 'start', + }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [ { index: 0 }, { index: 1 } ], + canScrollPrev: false, + canScrollNext: true, + ariaLabelPattern: 'Go to slide %d', + }; + + expect( context.autoplay ).toBe( false ); + expect( context.isPlaying ).toBe( false ); + } ); + + it( 'should accept context with autoplay configuration object', () => { + const context: CarouselContext = { + options: { + loop: true, + align: 'center', + }, + autoplay: { + delay: 5000, + stopOnInteraction: true, + stopOnMouseEnter: false, + }, + isPlaying: true, + timerIterationId: 1, + selectedIndex: 2, + scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], + canScrollPrev: true, + canScrollNext: true, + ariaLabelPattern: 'Slide %d of 3', + }; + + expect( context.autoplay ).not.toBe( false ); + expect( typeof context.autoplay ).toBe( 'object' ); + + if ( typeof context.autoplay === 'object' ) { + expect( context.autoplay.delay ).toBe( 5000 ); + expect( context.autoplay.stopOnInteraction ).toBe( true ); + expect( context.autoplay.stopOnMouseEnter ).toBe( false ); + } + } ); + } ); + + describe( 'Scroll State Management', () => { + it( 'should track first slide state correctly', () => { + const context: CarouselContext = { + options: { loop: false }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], + canScrollPrev: false, + canScrollNext: true, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.selectedIndex ).toBe( 0 ); + expect( context.canScrollPrev ).toBe( false ); + expect( context.canScrollNext ).toBe( true ); + } ); + + it( 'should track middle slide state correctly', () => { + const context: CarouselContext = { + options: { loop: false }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 1, + scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], + canScrollPrev: true, + canScrollNext: true, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.selectedIndex ).toBe( 1 ); + expect( context.canScrollPrev ).toBe( true ); + expect( context.canScrollNext ).toBe( true ); + } ); + + it( 'should track last slide state correctly', () => { + const context: CarouselContext = { + options: { loop: false }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 2, + scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], + canScrollPrev: true, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.selectedIndex ).toBe( 2 ); + expect( context.canScrollPrev ).toBe( true ); + expect( context.canScrollNext ).toBe( false ); + } ); + + it( 'should handle single slide carousel', () => { + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [ { index: 0 } ], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.scrollSnaps ).toHaveLength( 1 ); + expect( context.canScrollPrev ).toBe( false ); + expect( context.canScrollNext ).toBe( false ); + } ); + } ); + + describe( 'Timer and Animation State', () => { + it( 'should track timerIterationId for animation resets', () => { + const context: CarouselContext = { + options: {}, + autoplay: { delay: 3000, stopOnInteraction: true, stopOnMouseEnter: false }, + isPlaying: true, + timerIterationId: 5, + selectedIndex: 0, + scrollSnaps: [ { index: 0 } ], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.timerIterationId ).toBe( 5 ); + + // Simulate slide change incrementing the ID + const updatedContext = { ...context, timerIterationId: 6 }; + expect( updatedContext.timerIterationId ).toBe( 6 ); + } ); + + it( 'should have initial timerIterationId of 0', () => { + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.timerIterationId ).toBe( 0 ); + } ); + } ); + + describe( 'Optional Properties', () => { + it( 'should include optional ref property when provided', () => { + const element = document.createElement( 'div' ); + + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + ref: element, + }; + + expect( context.ref ).toBe( element ); + expect( context.ref ).toBeInstanceOf( HTMLElement ); + } ); + + it( 'should allow null ref', () => { + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + ref: null, + }; + + expect( context.ref ).toBeNull(); + } ); + + it( 'should work without ref property', () => { + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.ref ).toBeUndefined(); + } ); + } ); + + describe( 'Embla Options Integration', () => { + it( 'should accept slidesToScroll as number in options', () => { + const context: CarouselContext = { + options: { + loop: true, + slidesToScroll: 2, + }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.options.slidesToScroll ).toBe( 2 ); + } ); + + it( 'should accept slidesToScroll as "auto" in options', () => { + const context: CarouselContext = { + options: { + loop: true, + slidesToScroll: 'auto', + }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.options.slidesToScroll ).toBe( 'auto' ); + } ); + } ); + + describe( 'ARIA Label Pattern', () => { + it( 'should accept pattern with placeholder', () => { + const patterns = [ + 'Go to slide %d', + 'Slide %d of 10', + 'View item %d', + '%d', + ]; + + patterns.forEach( ( pattern ) => { + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: pattern, + }; + + expect( context.ariaLabelPattern ).toBe( pattern ); + } ); + } ); + } ); + + describe( 'Scroll Snaps Array', () => { + it( 'should accept empty scrollSnaps array', () => { + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [], + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.scrollSnaps ).toHaveLength( 0 ); + } ); + + it( 'should accept scrollSnaps with correct index structure', () => { + const scrollSnaps = [ + { index: 0 }, + { index: 1 }, + { index: 2 }, + { index: 3 }, + { index: 4 }, + ]; + + const context: CarouselContext = { + options: {}, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps, + canScrollPrev: false, + canScrollNext: false, + ariaLabelPattern: 'Slide %d', + }; + + expect( context.scrollSnaps ).toHaveLength( 5 ); + context.scrollSnaps.forEach( ( snap, i ) => { + expect( snap.index ).toBe( i ); + } ); + } ); + } ); +} ); diff --git a/src/blocks/carousel/__tests__/view.test.ts b/src/blocks/carousel/__tests__/view.test.ts new file mode 100644 index 0000000..ee8c22a --- /dev/null +++ b/src/blocks/carousel/__tests__/view.test.ts @@ -0,0 +1,775 @@ +/** + * Unit tests for the carousel view.ts frontend logic. + * + * Tests cover: + * - Store registration and configuration + * - State getters and their return values + * - Action behaviors (scrollPrev, scrollNext, onDotClick) + * - Callback behaviors (isSlideActive, isDotActive, getDotLabel, initCarousel) + * - Error handling and edge cases + * - Embla Carousel integration + * + * @package + */ + +import { + store, + getContext, + getElement, +} from '@wordpress/interactivity'; + +import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'; + +// Symbol key used by the view.ts for Embla instances +const EMBLA_KEY = Symbol.for( 'core-carousel.carousel' ); + +// Type for viewport element with Embla instance attached +type EmblaViewportElement = HTMLElement & { + [ EMBLA_KEY ]?: EmblaCarouselType; +}; + +import type { CarouselContext } from '../types'; + +// Import view to trigger store registration +import '../view'; + +// Get the store config that was passed to store() +const storeCall = ( store as jest.Mock ).mock.calls.find( + ( call: unknown[] ) => call[ 0 ] === 'core-carousel/carousel', +); +const storeConfig = storeCall ? storeCall[ 1 ] : null; + +/** + * Helper to set Embla instance on a viewport element. + * @param viewport The viewport element to attach the Embla instance to. + * @param embla The Embla instance to attach. + */ +const setEmblaOnViewport = ( + viewport: HTMLElement, + embla: Partial< EmblaCarouselType >, +) => { + ( viewport as EmblaViewportElement )[ EMBLA_KEY ] = + embla as EmblaCarouselType; +}; + +/** + * Helper to create mock carousel context with customizable properties. + * @param overrides Partial properties to override in the default context. + */ +const createMockContext = ( + overrides: Partial< CarouselContext > = {}, +): CarouselContext => ( { + options: { loop: true }, + autoplay: false, + isPlaying: false, + timerIterationId: 0, + selectedIndex: 0, + scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], + canScrollPrev: true, + canScrollNext: true, + ariaLabelPattern: 'Go to slide %d', + ...overrides, +} ); + +/** + * Helper to create a mock DOM element structure for carousel. + */ +const createMockCarouselDOM = () => { + const viewport = document.createElement( 'div' ); + viewport.className = 'embla'; + + const wrapper = document.createElement( 'div' ); + wrapper.className = 'core-carousel'; + wrapper.appendChild( viewport ); + + const button = document.createElement( 'button' ); + wrapper.appendChild( button ); + + return { wrapper, viewport, button }; +}; + +/** + * Helper to create mock Embla instance with all required methods. + * @param overrides Partial methods to override in the default mock instance. + */ +const createMockEmblaInstance = ( overrides = {} ) => ( { + scrollPrev: jest.fn(), + scrollNext: jest.fn(), + scrollTo: jest.fn(), + on: jest.fn(), + off: jest.fn(), + destroy: jest.fn(), + canScrollPrev: jest.fn( () => true ), + canScrollNext: jest.fn( () => true ), + selectedScrollSnap: jest.fn( () => 0 ), + scrollSnapList: jest.fn( () => [ 0, 0.5, 1 ] ), + ...overrides, +} ); + +describe( 'Carousel View Module', () => { + describe( 'Store Registration', () => { + it( 'should register store with correct namespace', () => { + // storeCall being defined proves store was called with the correct namespace + expect( storeCall ).toBeDefined(); + expect( storeConfig ).not.toBeNull(); + expect( storeCall[ 0 ] ).toBe( 'core-carousel/carousel' ); + } ); + + it( 'should register store with all required sections', () => { + expect( storeConfig ).toMatchObject( { + state: expect.any( Object ), + actions: expect.any( Object ), + callbacks: expect.any( Object ), + } ); + } ); + + it( 'should have state object defined with getters', () => { + expect( storeConfig?.state ).toBeDefined(); + expect( + Object.getOwnPropertyDescriptor( storeConfig.state, 'canScrollPrev' ) + ?.get, + ).toBeDefined(); + expect( + Object.getOwnPropertyDescriptor( storeConfig.state, 'canScrollNext' ) + ?.get, + ).toBeDefined(); + } ); + } ); + + describe( 'State Getters', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + describe( 'canScrollPrev', () => { + it( 'should return true when context.canScrollPrev is true', () => { + const mockContext = createMockContext( { canScrollPrev: true } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = + Object.getOwnPropertyDescriptor( + storeConfig.state, + 'canScrollPrev', + )?.get?.() ?? false; + + expect( result ).toBe( true ); + } ); + + it( 'should return false when context.canScrollPrev is false', () => { + const mockContext = createMockContext( { canScrollPrev: false } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = + Object.getOwnPropertyDescriptor( + storeConfig.state, + 'canScrollPrev', + )?.get?.() ?? true; + + expect( result ).toBe( false ); + } ); + } ); + + describe( 'canScrollNext', () => { + it( 'should return true when context.canScrollNext is true', () => { + const mockContext = createMockContext( { canScrollNext: true } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = + Object.getOwnPropertyDescriptor( + storeConfig.state, + 'canScrollNext', + )?.get?.() ?? false; + + expect( result ).toBe( true ); + } ); + + it( 'should return false when context.canScrollNext is false', () => { + const mockContext = createMockContext( { canScrollNext: false } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = + Object.getOwnPropertyDescriptor( + storeConfig.state, + 'canScrollNext', + )?.get?.() ?? true; + + expect( result ).toBe( false ); + } ); + } ); + } ); + + describe( 'Actions', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + describe( 'scrollPrev', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.actions?.scrollPrev ).toBeDefined(); + expect( typeof storeConfig?.actions?.scrollPrev ).toBe( 'function' ); + } ); + + it( 'should call embla.scrollPrev when embla instance exists', () => { + const { wrapper, viewport, button } = createMockCarouselDOM(); + const mockEmbla = createMockEmblaInstance(); + + // Set up embla instance on viewport using the helper + setEmblaOnViewport( viewport, mockEmbla ); + + ( getElement as jest.Mock ).mockReturnValue( { ref: button } ); + document.body.appendChild( wrapper ); + + storeConfig.actions.scrollPrev(); + + expect( mockEmbla.scrollPrev ).toHaveBeenCalledTimes( 1 ); + + document.body.removeChild( wrapper ); + } ); + + it( 'should log warning when embla instance not found', () => { + const consoleSpy = jest + .spyOn( console, 'warn' ) + .mockImplementation(); + const button = document.createElement( 'button' ); + + ( getElement as jest.Mock ).mockReturnValue( { ref: button } ); + + storeConfig.actions.scrollPrev(); + + expect( consoleSpy ).toHaveBeenCalledWith( + 'Carousel: Embla instance not found for scrollPrev', + ); + + consoleSpy.mockRestore(); + } ); + + it( 'should handle null element gracefully', () => { + const consoleSpy = jest + .spyOn( console, 'warn' ) + .mockImplementation(); + ( getElement as jest.Mock ).mockReturnValue( null ); + + expect( () => storeConfig.actions.scrollPrev() ).not.toThrow(); + expect( consoleSpy ).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + } ); + } ); + + describe( 'scrollNext', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.actions?.scrollNext ).toBeDefined(); + expect( typeof storeConfig?.actions?.scrollNext ).toBe( 'function' ); + } ); + + it( 'should call embla.scrollNext when embla instance exists', () => { + const { wrapper, viewport, button } = createMockCarouselDOM(); + const mockEmbla = createMockEmblaInstance(); + + setEmblaOnViewport( viewport, mockEmbla ); + + ( getElement as jest.Mock ).mockReturnValue( { ref: button } ); + document.body.appendChild( wrapper ); + + storeConfig.actions.scrollNext(); + + expect( mockEmbla.scrollNext ).toHaveBeenCalledTimes( 1 ); + + document.body.removeChild( wrapper ); + } ); + + it( 'should log warning when embla instance not found', () => { + const consoleSpy = jest + .spyOn( console, 'warn' ) + .mockImplementation(); + const button = document.createElement( 'button' ); + + ( getElement as jest.Mock ).mockReturnValue( { ref: button } ); + + storeConfig.actions.scrollNext(); + + expect( consoleSpy ).toHaveBeenCalledWith( + 'Carousel: Embla instance not found for scrollNext', + ); + + consoleSpy.mockRestore(); + } ); + } ); + + describe( 'onDotClick', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.actions?.onDotClick ).toBeDefined(); + expect( typeof storeConfig?.actions?.onDotClick ).toBe( 'function' ); + } ); + + it( 'should call embla.scrollTo with correct index', () => { + const { wrapper, viewport, button } = createMockCarouselDOM(); + const mockEmbla = createMockEmblaInstance(); + + setEmblaOnViewport( viewport, mockEmbla ); + + const mockContext = createMockContext(); + ( mockContext as CarouselContext & { snap?: { index: number } } ).snap = + { index: 2 }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: button } ); + document.body.appendChild( wrapper ); + + storeConfig.actions.onDotClick(); + + expect( mockEmbla.scrollTo ).toHaveBeenCalledWith( 2 ); + + document.body.removeChild( wrapper ); + } ); + + it( 'should not throw when snap is undefined', () => { + const mockContext = createMockContext(); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( null ); + + expect( () => storeConfig.actions.onDotClick() ).not.toThrow(); + } ); + + it( 'should handle snap.index of 0 correctly', () => { + const { wrapper, viewport, button } = createMockCarouselDOM(); + const mockEmbla = createMockEmblaInstance(); + + setEmblaOnViewport( viewport, mockEmbla ); + + const mockContext = createMockContext(); + ( mockContext as CarouselContext & { snap?: { index: number } } ).snap = + { index: 0 }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: button } ); + document.body.appendChild( wrapper ); + + storeConfig.actions.onDotClick(); + + expect( mockEmbla.scrollTo ).toHaveBeenCalledWith( 0 ); + + document.body.removeChild( wrapper ); + } ); + } ); + } ); + + describe( 'Callbacks', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + describe( 'isSlideActive', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.callbacks?.isSlideActive ).toBeDefined(); + expect( typeof storeConfig?.callbacks?.isSlideActive ).toBe( + 'function', + ); + } ); + + it( 'should return false when element is null', () => { + ( getElement as jest.Mock ).mockReturnValue( null ); + + const result = storeConfig.callbacks.isSlideActive(); + + expect( result ).toBe( false ); + } ); + + it( 'should return true when slide index matches selectedIndex', () => { + const container = document.createElement( 'div' ); + container.className = 'embla__container'; + + const slide1 = document.createElement( 'div' ); + slide1.className = 'embla__slide'; + const slide2 = document.createElement( 'div' ); + slide2.className = 'embla__slide'; + + container.appendChild( slide1 ); + container.appendChild( slide2 ); + + const mockContext = createMockContext( { selectedIndex: 1 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: slide2 } ); + + const result = storeConfig.callbacks.isSlideActive(); + + expect( result ).toBe( true ); + } ); + + it( 'should return false when slide index does not match selectedIndex', () => { + const container = document.createElement( 'div' ); + container.className = 'embla__container'; + + const slide1 = document.createElement( 'div' ); + slide1.className = 'embla__slide'; + const slide2 = document.createElement( 'div' ); + slide2.className = 'embla__slide'; + + container.appendChild( slide1 ); + container.appendChild( slide2 ); + + const mockContext = createMockContext( { selectedIndex: 0 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: slide2 } ); + + const result = storeConfig.callbacks.isSlideActive(); + + expect( result ).toBe( false ); + } ); + + it( 'should work with Query Loop posts (.wp-block-post)', () => { + const container = document.createElement( 'div' ); + + const post1 = document.createElement( 'li' ); + post1.className = 'wp-block-post'; + const post2 = document.createElement( 'li' ); + post2.className = 'wp-block-post'; + + container.appendChild( post1 ); + container.appendChild( post2 ); + + const mockContext = createMockContext( { selectedIndex: 0 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: post1 } ); + + const result = storeConfig.callbacks.isSlideActive(); + + expect( result ).toBe( true ); + } ); + } ); + + describe( 'isDotActive', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.callbacks?.isDotActive ).toBeDefined(); + expect( typeof storeConfig?.callbacks?.isDotActive ).toBe( 'function' ); + } ); + + it( 'should return true when snap.index matches selectedIndex', () => { + const mockContext = createMockContext( { selectedIndex: 2 } ); + ( mockContext as CarouselContext & { snap?: { index: number } } ).snap = + { index: 2 }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.isDotActive(); + + expect( result ).toBe( true ); + } ); + + it( 'should return false when snap.index does not match selectedIndex', () => { + const mockContext = createMockContext( { selectedIndex: 0 } ); + ( mockContext as CarouselContext & { snap?: { index: number } } ).snap = + { index: 2 }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.isDotActive(); + + expect( result ).toBe( false ); + } ); + } ); + + describe( 'getDotLabel', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.callbacks?.getDotLabel ).toBeDefined(); + expect( typeof storeConfig?.callbacks?.getDotLabel ).toBe( 'function' ); + } ); + + it( 'should return formatted label with correct index (1-based)', () => { + const mockContext = createMockContext( { + ariaLabelPattern: 'Go to slide %d', + } ); + ( mockContext as CarouselContext & { snap?: { index: number } } ).snap = + { index: 2 }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getDotLabel(); + + expect( result ).toBe( 'Go to slide 3' ); + } ); + + it( 'should handle first slide (index 0) correctly', () => { + const mockContext = createMockContext( { + ariaLabelPattern: 'Slide %d of 5', + } ); + ( mockContext as CarouselContext & { snap?: { index: number } } ).snap = + { index: 0 }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getDotLabel(); + + expect( result ).toBe( 'Slide 1 of 5' ); + } ); + + it( 'should handle undefined snap index', () => { + const mockContext = createMockContext( { + ariaLabelPattern: 'Go to slide %d', + } ); + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getDotLabel(); + + expect( result ).toBe( 'Go to slide 1' ); + } ); + } ); + + describe( 'initCarousel', () => { + it( 'should be defined as a function', () => { + expect( storeConfig?.callbacks?.initCarousel ).toBeDefined(); + expect( typeof storeConfig?.callbacks?.initCarousel ).toBe( + 'function', + ); + } ); + + it( 'should return early and log warning for invalid element', () => { + const consoleSpy = jest + .spyOn( console, 'warn' ) + .mockImplementation(); + const mockContext = createMockContext(); + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( null ); + + const result = storeConfig.callbacks.initCarousel(); + + expect( consoleSpy ).toHaveBeenCalledWith( + 'Carousel: Invalid root element', + null, + ); + expect( result ).toBeUndefined(); + + consoleSpy.mockRestore(); + } ); + + it( 'should log warning when viewport not found', () => { + const consoleSpy = jest + .spyOn( console, 'warn' ) + .mockImplementation(); + const mockContext = createMockContext(); + const element = document.createElement( 'div' ); + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: element } ); + + const result = storeConfig.callbacks.initCarousel(); + + expect( consoleSpy ).toHaveBeenCalledWith( + 'Carousel: Viewport (.embla) not found', + ); + expect( result ).toBeUndefined(); + + consoleSpy.mockRestore(); + } ); + + it( 'should handle errors gracefully and log them', () => { + const consoleErrorSpy = jest + .spyOn( console, 'error' ) + .mockImplementation(); + const mockContext = createMockContext(); + + // Create element that will cause an error + const element = { + querySelector: () => { + throw new Error( 'Test error' ); + }, + }; + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: element } ); + + const result = storeConfig.callbacks.initCarousel(); + + expect( consoleErrorSpy ).toHaveBeenCalledWith( + 'Carousel: Error in initCarousel', + expect.any( Error ), + ); + expect( result ).toBeNull(); + + consoleErrorSpy.mockRestore(); + } ); + } ); + } ); +} ); + +describe( 'Embla Carousel Integration', () => { + // Type-safe helper to get mocked Embla instance + const getMockedEmblaCarousel = () => + EmblaCarousel as unknown as jest.Mock; + + it( 'should have EmblaCarousel mocked', () => { + expect( EmblaCarousel ).toBeDefined(); + expect( jest.isMockFunction( EmblaCarousel ) ).toBe( true ); + } ); + + it( 'should create embla instance with correct methods', () => { + const mockInstance = getMockedEmblaCarousel()(); + + expect( mockInstance.scrollPrev ).toBeDefined(); + expect( mockInstance.scrollNext ).toBeDefined(); + expect( mockInstance.scrollTo ).toBeDefined(); + expect( mockInstance.on ).toBeDefined(); + expect( mockInstance.destroy ).toBeDefined(); + } ); + + it( 'should have scrollPrev as a callable function', () => { + const mockInstance = getMockedEmblaCarousel()(); + + expect( () => mockInstance.scrollPrev() ).not.toThrow(); + } ); + + it( 'should have scrollNext as a callable function', () => { + const mockInstance = getMockedEmblaCarousel()(); + + expect( () => mockInstance.scrollNext() ).not.toThrow(); + } ); + + it( 'should have scrollTo as a callable function', () => { + const mockInstance = getMockedEmblaCarousel()(); + + expect( () => mockInstance.scrollTo( 2 ) ).not.toThrow(); + } ); + + it( 'should have on method for event listeners', () => { + const mockInstance = getMockedEmblaCarousel()(); + + expect( () => mockInstance.on( 'select', jest.fn() ) ).not.toThrow(); + } ); + + it( 'should have destroy method for cleanup', () => { + const mockInstance = getMockedEmblaCarousel()(); + + expect( () => mockInstance.destroy() ).not.toThrow(); + } ); +} ); + +describe( 'Element Reference Handling', () => { + describe( 'getElement variations', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should handle getElement returning HTMLElement directly', () => { + const consoleSpy = jest.spyOn( console, 'warn' ).mockImplementation(); + const element = document.createElement( 'div' ); + + ( getElement as jest.Mock ).mockReturnValue( element ); + + // This should not throw + expect( () => storeConfig.actions.scrollPrev() ).not.toThrow(); + + consoleSpy.mockRestore(); + } ); + + it( 'should handle getElement returning object with ref', () => { + const consoleSpy = jest.spyOn( console, 'warn' ).mockImplementation(); + const element = document.createElement( 'div' ); + + ( getElement as jest.Mock ).mockReturnValue( { ref: element } ); + + expect( () => storeConfig.actions.scrollPrev() ).not.toThrow(); + + consoleSpy.mockRestore(); + } ); + + it( 'should handle getElement returning object with null ref', () => { + const consoleSpy = jest.spyOn( console, 'warn' ).mockImplementation(); + + ( getElement as jest.Mock ).mockReturnValue( { ref: null } ); + + expect( () => storeConfig.actions.scrollPrev() ).not.toThrow(); + + consoleSpy.mockRestore(); + } ); + + it( 'should handle getElement returning undefined', () => { + const consoleSpy = jest.spyOn( console, 'warn' ).mockImplementation(); + + ( getElement as jest.Mock ).mockReturnValue( undefined ); + + expect( () => storeConfig.actions.scrollPrev() ).not.toThrow(); + + consoleSpy.mockRestore(); + } ); + } ); +} ); + +describe( 'Edge Cases and Error Handling', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should handle carousel with no slides gracefully', () => { + const container = document.createElement( 'div' ); + container.className = 'embla__container'; + + const mockContext = createMockContext( { + selectedIndex: 0, + scrollSnaps: [], + } ); + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { + ref: container, + } ); + + // Should not throw + expect( () => storeConfig.callbacks.isSlideActive() ).not.toThrow(); + } ); + + it( 'should handle RTL direction in context', () => { + const mockContext = createMockContext( { + options: { direction: 'rtl', loop: true }, + } ); + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + expect( mockContext.options.direction ).toBe( 'rtl' ); + } ); + + it( 'should handle autoplay configuration', () => { + const mockContextWithAutoplay = createMockContext( { + autoplay: { + delay: 3000, + stopOnInteraction: true, + stopOnMouseEnter: false, + }, + isPlaying: true, + } ); + + expect( mockContextWithAutoplay.autoplay ).toEqual( { + delay: 3000, + stopOnInteraction: true, + stopOnMouseEnter: false, + } ); + expect( mockContextWithAutoplay.isPlaying ).toBe( true ); + } ); + + it( 'should handle timerIterationId for animation resets', () => { + const mockContext = createMockContext( { + timerIterationId: 5, + } ); + + expect( mockContext.timerIterationId ).toBe( 5 ); + + // Simulate increment on slide change + mockContext.timerIterationId += 1; + + expect( mockContext.timerIterationId ).toBe( 6 ); + } ); + + it( 'should handle large number of slides', () => { + const scrollSnaps = Array.from( { length: 100 }, ( _, i ) => ( { + index: i, + } ) ); + const mockContext = createMockContext( { + scrollSnaps, + selectedIndex: 50, + } ); + + expect( mockContext.scrollSnaps ).toHaveLength( 100 ); + expect( mockContext.selectedIndex ).toBe( 50 ); + } ); +} ); diff --git a/tests/js/__mocks__/wordpress-interactivity.ts b/tests/js/__mocks__/wordpress-interactivity.ts new file mode 100644 index 0000000..460b66d --- /dev/null +++ b/tests/js/__mocks__/wordpress-interactivity.ts @@ -0,0 +1,8 @@ +/** + * Mock for @wordpress/interactivity module. + */ + +export const store = jest.fn( ( _namespace: string, config: unknown ) => config ); +export const getContext = jest.fn( () => ( {} ) ); +export const getElement = jest.fn( () => ( { ref: null } ) ); +export const withScope = jest.fn( ( callback: () => void ) => callback ); diff --git a/tests/js/setup.ts b/tests/js/setup.ts new file mode 100644 index 0000000..a95a3c0 --- /dev/null +++ b/tests/js/setup.ts @@ -0,0 +1,156 @@ +/** + * Jest setup file for Core Carousel tests. + * + * This file runs before each test file and sets up global mocks + * for WordPress and Embla Carousel dependencies. + */ + +import '@testing-library/jest-dom'; + +/** + * Mock embla-carousel module. + */ +jest.mock( 'embla-carousel', () => ( { + __esModule: true, + default: jest.fn( () => ( { + canScrollPrev: jest.fn( () => true ), + canScrollNext: jest.fn( () => true ), + scrollPrev: jest.fn(), + scrollNext: jest.fn(), + scrollTo: jest.fn(), + selectedScrollSnap: jest.fn( () => 0 ), + scrollSnapList: jest.fn( () => [ 0, 1, 2 ] ), + slideNodes: jest.fn( () => [] ), + on: jest.fn(), + off: jest.fn(), + destroy: jest.fn(), + reInit: jest.fn(), + rootNode: jest.fn(), + containerNode: jest.fn(), + plugins: jest.fn( () => ( {} ) ), + } ) ), +} ) ); + +/** + * Mock embla-carousel-autoplay module. + */ +jest.mock( 'embla-carousel-autoplay', () => ( { + __esModule: true, + default: jest.fn( () => ( { + name: 'autoplay', + options: { delay: 3000 }, + init: jest.fn(), + destroy: jest.fn(), + play: jest.fn(), + stop: jest.fn(), + reset: jest.fn(), + isPlaying: jest.fn( () => false ), + } ) ), +} ) ); + +/** + * Mock WordPress block editor components. + */ +jest.mock( '@wordpress/block-editor', () => ( { + useBlockProps: jest.fn( ( props = {} ) => ( { + className: 'wp-block', + ...props, + } ) ), + useInnerBlocksProps: jest.fn( ( blockProps = {} ) => ( { + ...blockProps, + children: null, + } ) ), + InspectorControls: jest.fn( ( { children } ) => children ), + BlockControls: jest.fn( ( { children } ) => children ), + InnerBlocks: { + Content: jest.fn( () => null ), + }, + store: { + name: 'core/block-editor', + }, +} ) ); + +/** + * Mock WordPress components. + */ +jest.mock( '@wordpress/components', () => ( { + PanelBody: jest.fn( ( { children } ) => children ), + ToggleControl: jest.fn( () => null ), + RangeControl: jest.fn( () => null ), + SelectControl: jest.fn( () => null ), + Button: jest.fn( ( { children } ) => children ), + Flex: jest.fn( ( { children } ) => children ), + FlexItem: jest.fn( ( { children } ) => children ), + FlexBlock: jest.fn( ( { children } ) => children ), + __experimentalUnitControl: jest.fn( () => null ), +} ) ); + +/** + * Mock WordPress data module. + */ +jest.mock( '@wordpress/data', () => ( { + useSelect: jest.fn( () => ( {} ) ), + useDispatch: jest.fn( () => ( {} ) ), + select: jest.fn( () => ( {} ) ), + dispatch: jest.fn( () => ( {} ) ), + subscribe: jest.fn(), + createReduxStore: jest.fn(), + register: jest.fn(), +} ) ); + +/** + * Mock WordPress i18n module. + */ +jest.mock( '@wordpress/i18n', () => ( { + __: jest.fn( ( text: string ) => text ), + _x: jest.fn( ( text: string ) => text ), + _n: jest.fn( ( single: string ) => single ), + sprintf: jest.fn( ( format: string, ...args: unknown[] ) => { + let result = format; + args.forEach( ( arg ) => { + result = result.replace( '%s', String( arg ) ); + result = result.replace( '%d', String( arg ) ); + } ); + return result; + } ), +} ) ); + +/** + * Mock WordPress element module. + */ +jest.mock( '@wordpress/element', () => { + const actualReact = jest.requireActual( 'react' ); + return { + ...actualReact, + createInterpolateElement: jest.fn( ( text: string ) => text ), + RawHTML: jest.fn( ( { children } ) => children ), + }; +} ); + +/** + * Helper to reset all mocks between tests. + */ +beforeEach( () => { + jest.clearAllMocks(); +} ); + +/** + * Helper factory to create mock Embla instance for use in tests. + */ +export const createMockEmblaInstance = () => ( { + canScrollPrev: jest.fn( () => true ), + canScrollNext: jest.fn( () => true ), + scrollPrev: jest.fn(), + scrollNext: jest.fn(), + scrollTo: jest.fn(), + selectedScrollSnap: jest.fn( () => 0 ), + scrollSnapList: jest.fn( () => [ 0, 1, 2 ] ), + slideNodes: jest.fn( () => [] ), + on: jest.fn(), + off: jest.fn(), + destroy: jest.fn(), + reInit: jest.fn(), + rootNode: jest.fn( () => document.createElement( 'div' ) ), + containerNode: jest.fn( () => document.createElement( 'div' ) ), + plugins: jest.fn( () => ( {} ) ), +} ); diff --git a/tests/js/tsconfig.json b/tests/js/tsconfig.json new file mode 100644 index 0000000..169d5dd --- /dev/null +++ b/tests/js/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../_output/js", + "noEmit": true, + "types": ["jest", "node"] + }, + "include": ["./**/*.ts", "./**/*.tsx"] +} diff --git a/tests/php/Unit/PluginTest.php b/tests/php/Unit/PluginTest.php new file mode 100644 index 0000000..7d60751 --- /dev/null +++ b/tests/php/Unit/PluginTest.php @@ -0,0 +1,410 @@ + + */ + private const EXPECTED_BLOCKS = [ + 'carousel', + 'carousel/controls', + 'carousel/dots', + 'carousel/viewport', + 'carousel/slide', + ]; + + /** + * Helper to get Plugin instance via reflection without triggering singleton. + * + * @return Plugin + */ + private function getPluginInstance(): Plugin { + $reflection = new \ReflectionClass( Plugin::class ); + return $reflection->newInstanceWithoutConstructor(); + } + + /** + * Helper to invoke a protected/private method on an object. + * + * @param object $object The object instance. + * @param string $methodName The method name to invoke. + * @param array $args Arguments to pass to the method. + * @return mixed The return value of the method. + */ + private function invokeMethod( object $object, string $methodName, array $args = [] ): mixed { + $reflection = new \ReflectionClass( $object ); + $method = $reflection->getMethod( $methodName ); + + return $method->invokeArgs( $object, $args ); + } + + /** + * Test that register_block_category adds the category. + * + * @return void + */ + public function test_register_block_category_adds_category(): void { + Functions\when( '__' )->returnArg(); + + $instance = $this->getPluginInstance(); + + $existing_categories = [ + [ + 'slug' => 'text', + 'title' => 'Text', + ], + ]; + + $result = $this->invokeMethod( $instance, 'register_block_category', [ $existing_categories ] ); + + $this->assertCount( 2, $result ); + $this->assertSame( 'core-carousel', $result[1]['slug'] ); + $this->assertSame( 'Core Carousel', $result[1]['title'] ); + } + + /** + * Test that register_block_category preserves existing categories. + * + * @return void + */ + public function test_register_block_category_preserves_existing(): void { + Functions\when( '__' )->returnArg(); + + $instance = $this->getPluginInstance(); + + $existing_categories = [ + [ + 'slug' => 'media', + 'title' => 'Media', + ], + [ + 'slug' => 'design', + 'title' => 'Design', + ], + ]; + + $result = $this->invokeMethod( $instance, 'register_block_category', [ $existing_categories ] ); + + $this->assertCount( 3, $result ); + $this->assertSame( 'media', $result[0]['slug'] ); + $this->assertSame( 'design', $result[1]['slug'] ); + $this->assertSame( 'core-carousel', $result[2]['slug'] ); + } + + /** + * Test that category is added to empty categories array. + * + * @return void + */ + public function test_register_block_category_with_empty_array(): void { + Functions\when( '__' )->returnArg(); + + $instance = $this->getPluginInstance(); + $result = $this->invokeMethod( $instance, 'register_block_category', [ [] ] ); + + $this->assertCount( 1, $result ); + $this->assertSame( 'core-carousel', $result[0]['slug'] ); + } + + /** + * Test that register_blocks registers all expected blocks. + * + * @return void + */ + public function test_register_blocks_registers_all_blocks(): void { + $registered_blocks = []; + + Functions\when( 'register_block_type' )->alias( + function ( string $path ) use ( &$registered_blocks ): void { + $registered_blocks[] = $path; + } + ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_blocks' ); + + $this->assertCount( 5, $registered_blocks ); + + // Verify each expected block is registered + foreach ( self::EXPECTED_BLOCKS as $block ) { + $found = false; + foreach ( $registered_blocks as $path ) { + if ( str_contains( $path, "/blocks/{$block}" ) ) { + $found = true; + break; + } + } + $this->assertTrue( $found, "Block '{$block}' should be registered." ); + } + } + + /** + * Test that blocks are registered in the correct order. + * + * @return void + */ + public function test_register_blocks_in_correct_order(): void { + $registered_blocks = []; + + Functions\when( 'register_block_type' )->alias( + function ( string $path ) use ( &$registered_blocks ): void { + $registered_blocks[] = $path; + } + ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_blocks' ); + + $this->assertStringContainsString( '/blocks/carousel', $registered_blocks[0] ); + $this->assertStringContainsString( '/blocks/carousel/controls', $registered_blocks[1] ); + $this->assertStringContainsString( '/blocks/carousel/dots', $registered_blocks[2] ); + $this->assertStringContainsString( '/blocks/carousel/viewport', $registered_blocks[3] ); + $this->assertStringContainsString( '/blocks/carousel/slide', $registered_blocks[4] ); + } + + /** + * Test that register_blocks does nothing when build path is not defined. + * + * @return void + */ + public function test_register_blocks_handles_missing_build_path(): void { + // The actual behavior check: register_block_type should be called + // for each block when the constant is defined (as it is in our tests). + Functions\expect( 'register_block_type' )->times( 5 ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_blocks' ); + + // Assert that we got here without errors + $this->assertTrue( true ); + } + + /** + * Test that register_pattern_category registers the category. + * + * @return void + */ + public function test_register_pattern_category_registers_category(): void { + $category_registered = false; + + Functions\when( '__' )->returnArg(); + Functions\expect( 'register_block_pattern_category' ) + ->once() + ->with( + 'core-carousel', + \Mockery::type( 'array' ) + ) + ->andReturnUsing( + function () use ( &$category_registered ): void { + $category_registered = true; + } + ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_pattern_category' ); + + $this->assertTrue( $category_registered ); + } + + /** + * Test that pattern category includes proper label. + * + * @return void + */ + public function test_register_pattern_category_includes_label(): void { + Functions\when( '__' )->returnArg(); + + /** @var array|null $captured_args */ + $captured_args = null; + + Functions\expect( 'register_block_pattern_category' ) + ->once() + ->with( + 'core-carousel', + \Mockery::on( + function ( $args ) use ( &$captured_args ): bool { + $captured_args = $args; + return true; + } + ) + ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_pattern_category' ); + + $this->assertIsArray( $captured_args ); + $this->assertNotNull( $captured_args ); + $this->assertArrayHasKey( 'label', $captured_args ); + $this->assertSame( 'Core Carousel', $captured_args['label'] ); + } + + /** + * Test register_block_patterns uses cached patterns when available. + * + * @return void + */ + public function test_register_block_patterns_uses_cache(): void { + $cached_patterns = [ + [ + 'slug' => 'core-carousel/test-pattern', + 'args' => [ + 'title' => 'Test Pattern', + 'content' => '

Test

', + ], + ], + ]; + + $pattern_registered = false; + + Functions\when( '__' )->returnArg(); + Functions\expect( 'get_transient' ) + ->once() + ->with( 'core_carousel_patterns_cache' ) + ->andReturn( $cached_patterns ); + + Functions\expect( 'register_block_pattern' ) + ->once() + ->with( 'core-carousel/test-pattern', \Mockery::type( 'array' ) ) + ->andReturnUsing( + function () use ( &$pattern_registered ): void { + $pattern_registered = true; + } + ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_block_patterns' ); + + $this->assertTrue( $pattern_registered ); + } + + /** + * Test register_block_patterns handles empty patterns gracefully. + * + * @return void + */ + public function test_register_block_patterns_handles_empty(): void { + Functions\expect( 'get_transient' ) + ->once() + ->with( 'core_carousel_patterns_cache' ) + ->andReturn( [] ); + + Functions\expect( 'register_block_pattern' )->never(); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_block_patterns' ); + + // Assert method completed without registering patterns + $this->assertTrue( true ); + } + + /** + * Test register_block_patterns registers multiple patterns. + * + * @return void + */ + public function test_register_block_patterns_registers_multiple(): void { + $cached_patterns = [ + [ + 'slug' => 'core-carousel/pattern-one', + 'args' => [ + 'title' => 'Pattern One', + 'content' => '

One

', + ], + ], + [ + 'slug' => 'core-carousel/pattern-two', + 'args' => [ + 'title' => 'Pattern Two', + 'content' => '

Two

', + ], + ], + [ + 'slug' => 'core-carousel/pattern-three', + 'args' => [ + 'title' => 'Pattern Three', + 'content' => '

Three

', + ], + ], + ]; + + $registered_patterns = []; + + Functions\when( '__' )->returnArg(); + Functions\expect( 'get_transient' ) + ->once() + ->andReturn( $cached_patterns ); + + Functions\expect( 'register_block_pattern' ) + ->times( 3 ) + ->andReturnUsing( + function ( $slug ) use ( &$registered_patterns ): void { + $registered_patterns[] = $slug; + } + ); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_block_patterns' ); + + $this->assertCount( 3, $registered_patterns ); + $this->assertContains( 'core-carousel/pattern-one', $registered_patterns ); + $this->assertContains( 'core-carousel/pattern-two', $registered_patterns ); + $this->assertContains( 'core-carousel/pattern-three', $registered_patterns ); + } + + /** + * Test that patterns without required fields are handled. + * + * @return void + */ + public function test_register_block_patterns_handles_invalid_structure(): void { + // Pattern missing 'args' key + $cached_patterns = [ + [ + 'slug' => 'core-carousel/valid-pattern', + 'args' => [ + 'title' => 'Valid', + 'content' => '

Valid

', + ], + ], + ]; + + Functions\when( '__' )->returnArg(); + Functions\expect( 'get_transient' ) + ->once() + ->andReturn( $cached_patterns ); + + Functions\expect( 'register_block_pattern' )->once(); + + $instance = $this->getPluginInstance(); + $this->invokeMethod( $instance, 'register_block_patterns' ); + + // Assert completed successfully + $this->assertTrue( true ); + } +} diff --git a/tests/php/Unit/Traits/SingletonTest.php b/tests/php/Unit/Traits/SingletonTest.php new file mode 100644 index 0000000..92511a6 --- /dev/null +++ b/tests/php/Unit/Traits/SingletonTest.php @@ -0,0 +1,92 @@ +constructor_called = true; + } +} + +/** + * Test class B for singleton tests. + */ +class SingletonTestClassB { + use Singleton; +} + +/** + * Test class C for singleton tests. + */ +class SingletonTestClassC { + use Singleton; + + public bool $constructor_called = false; + + protected function __construct() { + $this->constructor_called = true; + } +} + +/** + * Tests for the Singleton trait. + */ +class SingletonTest extends UnitTestCase { + /** + * Test that get_instance returns an instance of the class. + * + * @return void + */ + public function test_get_instance_returns_instance(): void { + Actions\expectDone( 'core_carousel_singleton_init_' . SingletonTestClassA::class )->once(); + + $instance = SingletonTestClassA::get_instance(); + + $this->assertInstanceOf( SingletonTestClassA::class, $instance ); + } + + /** + * Test that get_instance returns the same instance on subsequent calls. + * + * @return void + */ + public function test_get_instance_returns_same_instance(): void { + Actions\expectDone( 'core_carousel_singleton_init_' . SingletonTestClassB::class )->once(); + + $instance1 = SingletonTestClassB::get_instance(); + $instance2 = SingletonTestClassB::get_instance(); + + $this->assertSame( $instance1, $instance2 ); + } + + /** + * Test that constructor is called on first instantiation. + * + * @return void + */ + public function test_constructor_is_called(): void { + Actions\expectDone( 'core_carousel_singleton_init_' . SingletonTestClassC::class )->once(); + + $instance = SingletonTestClassC::get_instance(); + + $this->assertTrue( $instance->constructor_called ); + } +} diff --git a/tests/php/Unit/UnitTestCase.php b/tests/php/Unit/UnitTestCase.php new file mode 100644 index 0000000..da71133 --- /dev/null +++ b/tests/php/Unit/UnitTestCase.php @@ -0,0 +1,38 @@ +