Compare commits

..

50 Commits

Author SHA1 Message Date
5d42e8fe3a release: v4.4.2 2026-03-10 21:09:12 +00:00
9d17b872dd chore: update stubs 2026-03-10 21:09:02 +00:00
2a80101f42 chore: update styling 2026-03-10 21:06:28 +00:00
f7015fe59c chore: adjusts sponsors 2026-02-24 10:44:48 +00:00
7281e0ded7 Add SerpApi to the list of sponsors
Add SerpApi as a sponsor in the README.
2026-02-20 01:18:43 +00:00
5de8693e3b chore: style 2026-02-17 16:15:58 +00:00
7d80f1d20e chore: removes non used files 2026-02-17 15:34:32 +00:00
b3119cc120 Merge pull request #1562 from michaelw85/patch-1
[Fix] Pass test dir to worker
2026-02-17 15:33:16 +00:00
4e294edf76 Merge pull request #1639 from imliam/patch-1
[Laravel Preset] Allow App\Http to be used in providers
2026-02-17 15:31:01 +00:00
f96a1b2786 release: v4.4.1 2026-02-17 15:27:18 +00:00
a49cf7edc5 ci: speed up ci 2026-02-17 15:21:20 +00:00
b0f6a74cb6 ci: makes jobs faster 2026-02-17 15:18:33 +00:00
aaa226f6a6 chore: tests against symfony 8 2026-02-17 15:14:45 +00:00
69cb752d02 chore: bumps dependencies 2026-02-17 15:01:37 +00:00
cf00e58b7d chore: bumps dependencies 2026-02-17 11:22:04 +00:00
1f39b28e2c Allow App\Http to be used in providers 2026-02-16 00:25:47 +01:00
9fcbca69d4 Update README.md 2026-02-13 10:41:22 +00:00
3a4329ddc7 release: 4.3.2 2026-01-28 01:01:19 +00:00
dd01229d7b Merge pull request #1606 from smirok/teamcity-fix-for-describe-tests-with-dataset
fix: replace `substr` with `mb_substr` in Str::beforeLast to ensure multibyte string compatibility and correct TeamCity test names for datasets in "describe" blocks
2026-01-15 01:52:01 +00:00
c7e4efcea4 fix: replace substr with mb_substr in Str::beforeLast to ensure multibyte string compatibility and correct TeamCity test names for datasets in "describe" blocks 2026-01-14 00:55:35 +01:00
df3205e814 Merge pull request #1554 from pindab0ter/feature/extend-closure-this
Specify closure this for extend
2026-01-13 01:19:47 +00:00
bc57a84e77 release: 4.3.1 2026-01-04 11:29:59 -05:00
bc39830d8a chore: removes toHaveSuspiciousCharacters from php preset 2026-01-04 11:25:57 -05:00
3a566b100e docs: why php 2026-01-04 11:04:03 -05:00
9fe61e0e56 docs: update sponsors
Removed and updated sponsor links in the README.
2026-01-03 18:07:02 -05:00
e86bec3e68 release: 4.3.0 2025-12-30 14:48:33 -05:00
58b8f3cc5d Merge pull request #1598 from Willem-Jaap/willem-jaap/pest-only-file-level
feat: add pest only function to mark each test in a file as only
2025-12-30 14:40:04 -05:00
c157b661f2 style: format 2025-12-30 09:26:35 +01:00
be90610f17 feat: add pest only function to mark each test in a file as only 2025-12-30 09:24:05 +01:00
1701a306c3 Merge pull request #1590 from pestphp/feature/intl-exception
feat: show more useful exception when `intl` extension not found
2025-12-29 22:09:16 -05:00
064ab3fc2e Merge pull request #1595 from bilboque/dirty-flag-not-found-in-help
feat: add --dirty documentation in --help
2025-12-29 22:08:12 -05:00
leo
44e315df98 feat: add --dirty documentation in --help 2025-12-22 11:02:28 +01:00
62694c14b9 chore: style 2025-12-15 11:54:24 +00:00
7c43c1c583 Merge pull request #1586 from jackbayliss/bump-checkout-action
[4.x] Bump checkout version from 5 to 6
2025-12-15 11:49:28 +00:00
6a96aed654 feat: adds phpunit@12.5 support 2025-12-15 11:48:43 +00:00
b1c997a869 feat: show more useful exception when intl extension not found 2025-12-12 12:02:00 +00:00
b4172e2c2e bump checkout version from 5 to 6 2025-12-10 14:08:06 +00:00
ae419afd36 chore: support for symfony 8.0.0 components 2025-11-28 12:04:48 +00:00
27aa305897 Merge pull request #1576 from Chris53897/feature/ci
ci: bump actions/checkout 4 => 5
2025-11-25 11:18:28 +00:00
f5820bd670 release: 4.1.5 2025-11-24 12:46:38 +00:00
41fd831153 release: 4.1.4 2025-11-24 10:25:45 +00:00
51340439e8 ci: bump actions/checkout 4 => 5 2025-11-22 12:12:15 +01:00
1a39826935 ci: tests against php 8.5 2025-11-20 02:59:27 +00:00
ae1da79ac1 Pass test dir to worker
#1444 
Test directory argument is lost when spawning workers, add it again.
2025-11-05 17:15:51 +01:00
00990efc97 Merge pull request #1544 from Se7en-RU/fix-testdox-columns-warning
Fix Undefined array key "testdox-columns" warning
2025-11-04 07:42:57 +00:00
477d20a54f release: 4.1.3 2025-10-29 22:45:27 +00:00
b7b16096db Specify closure this for extend 2025-10-29 11:20:08 +01:00
4105e33c39 Fix Undefined array key "testdox-columns" warning
Fix Undefined array key "testdox-columns" warning
2025-10-21 11:44:55 +03:00
08b09f2e98 release: 4.1.2 2025-10-05 20:09:49 +01:00
b0fab7e437 chore: uses phpunit@12.4 2025-10-05 20:09:42 +01:00
96 changed files with 245 additions and 141 deletions

View File

@ -2,10 +2,15 @@ name: Static Analysis
on: on:
push: push:
branches: [4.x]
pull_request: pull_request:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
concurrency:
group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
static: static:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
@ -19,7 +24,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@ -29,8 +34,22 @@ jobs:
coverage: none coverage: none
extensions: sockets extensions: sockets
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
static-php-8.3-${{ matrix.dependency-version }}-composer-
static-php-8.3-composer-
- name: Install Dependencies - name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Profanity Check - name: Profanity Check
run: composer test:profanity run: composer test:profanity

View File

@ -2,8 +2,13 @@ name: Tests
on: on:
push: push:
branches: [4.x]
pull_request: pull_request:
concurrency:
group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
tests: tests:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
@ -13,15 +18,18 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest] # windows-latest os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['7.3'] symfony: ['7.4', '8.0']
php: ['8.3', '8.4'] php: ['8.3', '8.4', '8.5']
dependency_version: [prefer-stable] dependency_version: [prefer-stable]
exclude:
- php: '8.3'
symfony: '8.0'
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@ -31,6 +39,20 @@ jobs:
coverage: none coverage: none
extensions: sockets extensions: sockets
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-
${{ matrix.os }}-php-${{ matrix.php }}-composer-
- name: Setup Problem Matches - name: Setup Problem Matches
run: | run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/php.json"

View File

@ -1,14 +0,0 @@
# Well documented Makefiles
DEFAULT_GOAL := help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
build: ## Build all docker images. Specify the command e.g. via make build ARGS="--build-arg PHP=8.2"
docker compose build $(ARGS)
##@ [Application]
install: ## Install the composer dependencies
docker compose run --rm composer install
test: ## Run the tests
docker compose run --rm composer test

View File

@ -5,6 +5,7 @@
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
</p> </p>
</p> </p>
@ -30,25 +31,23 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Platinum Sponsors ### Platinum Sponsors
- **[Laracasts](https://laracasts.com/?ref=pestphp)** - **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)** - **[Mailtrap](https://l.rw.rw/pestphp)**
- **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
- **[Tighten](https://tighten.com/?ref=nunomaduro)**
- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)**
### Gold Sponsors ### Gold Sponsors
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[CMS Max](https://cmsmax.com/?ref=pestphp)** - **[CMS Max](https://cmsmax.com/?ref=pestphp)**
### Premium Sponsors ### Premium Sponsors
- [Forge](https://forge.laravel.com/?ref=pestphp) - [Zapiet](https://zapiet.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp)
- [Load Forge](https://loadforge.com/?ref=pestphp) - [Load Forge](https://loadforge.com/?ref=pestphp)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) - [Route4Me](https://route4me.com/pt?ref=pestphp)
- [Route4Me](https://www.route4me.com/?ref=pestphp) - [Nerdify](https://getnerdify.com/?ref=pestphp)
- [Devtools for Livewire](https://devtools-for-livewire.com/?ref=pestphp)
- [Nerdify](https://www.getnerdify.com/?ref=pestphp)
- [Akaunting](https://akaunting.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp)
- [LambdaTest](https://lambdatest.com/?ref=pestphp) - [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -86,7 +86,7 @@ $bootPest = (static function (): void {
$getopt['teamcity-file'] ?? null, $getopt['teamcity-file'] ?? null,
$getopt['testdox-file'] ?? null, $getopt['testdox-file'] ?? null,
isset($getopt['testdox-color']), isset($getopt['testdox-color']),
(int) $getopt['testdox-columns'] ?? null, (int) ($getopt['testdox-columns'] ?? null),
); );
while (true) { while (true) {

View File

@ -18,19 +18,19 @@
], ],
"require": { "require": {
"php": "^8.3.0", "php": "^8.3.0",
"brianium/paratest": "^7.14.0", "brianium/paratest": "^7.19.0",
"nunomaduro/collision": "^8.8.2", "nunomaduro/collision": "^8.9.1",
"nunomaduro/termwind": "^2.3.1", "nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0",
"pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^4.1.0", "pestphp/pest-plugin-profanity": "^4.2.1",
"phpunit/phpunit": "^12.3.15", "phpunit/phpunit": "^12.5.12",
"symfony/process": "^7.3.3" "symfony/process": "^7.4.5|^8.0.5"
}, },
"conflict": { "conflict": {
"filp/whoops": "<2.18.3", "filp/whoops": "<2.18.3",
"phpunit/phpunit": ">12.3.15", "phpunit/phpunit": ">12.5.12",
"sebastian/exporter": "<7.0.0", "sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
@ -55,10 +55,10 @@
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-browser": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.0",
"pestphp/pest-plugin-type-coverage": "^4.0.2", "pestphp/pest-plugin-type-coverage": "^4.0.3",
"psy/psysh": "^0.12.10" "psy/psysh": "^0.12.21"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,

View File

@ -1,14 +0,0 @@
version: "3.8"
services:
php:
build:
context: ./docker
volumes:
- .:/var/www/html
composer:
build:
context: ./docker
volumes:
- .:/var/www/html
entrypoint: ["composer"]

View File

@ -14,6 +14,9 @@ namespace PHPUnit\Logging\JUnit;
use DOMDocument; use DOMDocument;
use DOMElement; use DOMElement;
use Pest\Logging\Converter;
use Pest\Support\Container;
use Pest\TestSuite;
use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\EventFacadeIsSealedException;
@ -50,7 +53,7 @@ final class JunitXmlLogger
{ {
private readonly Printer $printer; private readonly Printer $printer;
private readonly \Pest\Logging\Converter $converter; // pest-added private readonly Converter $converter; // pest-added
private DOMDocument $document; private DOMDocument $document;
@ -108,7 +111,7 @@ final class JunitXmlLogger
public function __construct(Printer $printer, Facade $facade) public function __construct(Printer $printer, Facade $facade)
{ {
$this->printer = $printer; $this->printer = $printer;
$this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added $this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath); // pest-added
$this->registerSubscribers($facade); $this->registerSubscribers($facade);
$this->createDocument(); $this->createDocument();

View File

@ -2,7 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig; use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure() return RectorConfig::configure()
@ -12,6 +15,9 @@ return RectorConfig::configure()
->withSkip([ ->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
ReturnNeverTypeRector::class, ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class,
RemoveParentDelegatingConstructorRector::class,
]) ])
->withPreparedSets( ->withPreparedSets(
deadCode: true, deadCode: true,

View File

@ -150,7 +150,7 @@ final class Laravel extends AbstractPreset
->toHaveSuffix('Controller'); ->toHaveSuffix('Controller');
$this->expectations[] = expect('App\Http') $this->expectations[] = expect('App\Http')
->toOnlyBeUsedIn('App\Http'); ->toOnlyBeUsedIn(['App\Http', 'App\Providers']);
$this->expectations[] = expect('App\Http\Controllers') $this->expectations[] = expect('App\Http\Controllers')
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']); ->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']);

View File

@ -4,9 +4,6 @@ declare(strict_types=1);
namespace Pest\ArchPresets; namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/** /**
* @internal * @internal
*/ */
@ -92,9 +89,5 @@ final class Php extends AbstractPreset
'xdebug_var_dump', 'xdebug_var_dump',
'trap', 'trap',
])->not->toBeUsed(); ])->not->toBeUsed();
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->not->toHaveSuspiciousCharacters(),
);
} }
} }

View File

@ -83,7 +83,7 @@ final class BootFiles implements Bootstrapper
private function bootDatasets(string $testsPath): void private function bootDatasets(string $testsPath): void
{ {
assert(strlen($testsPath) > 0); assert($testsPath !== '');
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');

View File

@ -8,6 +8,8 @@ use Closure;
/** /**
* @internal * @internal
*
* @template T of object
*/ */
trait Extendable trait Extendable
{ {
@ -20,6 +22,8 @@ trait Extendable
/** /**
* Register a new extend. * Register a new extend.
*
* @param-closure-this T $extend
*/ */
public function extend(string $name, Closure $extend): void public function extend(string $name, Closure $extend): void
{ {

View File

@ -66,6 +66,6 @@ trait Pipeable
*/ */
private function pipes(string $name, object $context, string $scope): array private function pipes(string $name, object $context, string $scope): array
{ {
return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
} }
} }

View File

@ -129,7 +129,7 @@ trait Testable
*/ */
public function __addBeforeAll(?Closure $hook): void public function __addBeforeAll(?Closure $hook): void
{ {
if (! $hook instanceof \Closure) { if (! $hook instanceof Closure) {
return; return;
} }
@ -143,7 +143,7 @@ trait Testable
*/ */
public function __addAfterAll(?Closure $hook): void public function __addAfterAll(?Closure $hook): void
{ {
if (! $hook instanceof \Closure) { if (! $hook instanceof Closure) {
return; return;
} }
@ -173,7 +173,7 @@ trait Testable
*/ */
private function __addHook(string $property, ?Closure $hook): void private function __addHook(string $property, ?Closure $hook): void
{ {
if (! $hook instanceof \Closure) { if (! $hook instanceof Closure) {
return; return;
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\UsesCall; use Pest\PendingCalls\UsesCall;
/** /**
@ -62,6 +63,14 @@ final readonly class Configuration
return (new UsesCall($this->filename, []))->group(...$groups); return (new UsesCall($this->filename, []))->group(...$groups);
} }
/**
* Marks all tests in the current file to be run exclusively.
*/
public function only(): void
{
(new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
}
/** /**
* Depending on where is called, it will extend the given classes and traits globally or locally. * Depending on where is called, it will extend the given classes and traits globally or locally.
*/ */

View File

@ -52,7 +52,9 @@ use ReflectionProperty;
*/ */
final class Expectation final class Expectation
{ {
/** @use Extendable<self<TValue>> */
use Extendable; use Extendable;
use Pipeable; use Pipeable;
use Retrievable; use Retrievable;
@ -134,7 +136,7 @@ final class Expectation
/** /**
* Dump the expectation value when the result of the condition is truthy. * Dump the expectation value when the result of the condition is truthy.
* *
* @param (\Closure(TValue): bool)|bool $condition * @param (Closure(TValue): bool)|bool $condition
* @return self<TValue> * @return self<TValue>
*/ */
public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation
@ -151,7 +153,7 @@ final class Expectation
/** /**
* Dump the expectation value when the result of the condition is falsy. * Dump the expectation value when the result of the condition is falsy.
* *
* @param (\Closure(TValue): bool)|bool $condition * @param (Closure(TValue): bool)|bool $condition
* @return self<TValue> * @return self<TValue>
*/ */
public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation
@ -397,7 +399,7 @@ final class Expectation
* *
* @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue * @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue
*/ */
public function __get(string $name) public function __get(string $name): mixed
{ {
if (! self::hasMethod($name)) { if (! self::hasMethod($name)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) { if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) {

View File

@ -15,6 +15,7 @@ use Pest\Arch\PendingArchExpectation;
use Pest\Arch\SingleArchExpectation; use Pest\Arch\SingleArchExpectation;
use Pest\Arch\Support\FileLineFinder; use Pest\Arch\Support\FileLineFinder;
use Pest\Exceptions\InvalidExpectation; use Pest\Exceptions\InvalidExpectation;
use Pest\Exceptions\MissingDependency;
use Pest\Expectation; use Pest\Expectation;
use Pest\Support\Arr; use Pest\Support\Arr;
use Pest\Support\Exporter; use Pest\Support\Exporter;
@ -284,6 +285,10 @@ final readonly class OppositeExpectation
*/ */
public function toHaveSuspiciousCharacters(): ArchExpectation public function toHaveSuspiciousCharacters(): ArchExpectation
{ {
if (! class_exists(Spoofchecker::class)) {
throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0');
}
$checker = new Spoofchecker; $checker = new Spoofchecker;
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */

View File

@ -17,6 +17,7 @@ use Pest\Factories\Concerns\HigherOrderable;
use Pest\Support\Reflection; use Pest\Support\Reflection;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RuntimeException; use RuntimeException;
@ -135,7 +136,7 @@ final class TestCaseFactory
$this->attributes = [ $this->attributes = [
new Attribute( new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class, TestDox::class,
[$this->filename], [$this->filename],
), ),
...$this->attributes, ...$this->attributes,

View File

@ -9,10 +9,14 @@ use Pest\Evaluators\Attributes;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Factories\Concerns\HigherOrderable; use Pest\Factories\Concerns\HigherOrderable;
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
use Pest\Support\Description;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -32,7 +36,7 @@ final class TestCaseMethodFactory
/** /**
* The test's describing, if any. * The test's describing, if any.
* *
* @var array<int, \Pest\Support\Description> * @var array<int, Description>
*/ */
public array $describing = []; public array $describing = [];
@ -192,11 +196,11 @@ final class TestCaseMethodFactory
$this->attributes = [ $this->attributes = [
new Attribute( new Attribute(
\PHPUnit\Framework\Attributes\Test::class, Test::class,
[], [],
), ),
new Attribute( new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class, TestDox::class,
[str_replace('*/', '{@*}', $this->description)], [str_replace('*/', '{@*}', $this->description)],
), ),
...$this->attributes, ...$this->attributes,
@ -206,7 +210,7 @@ final class TestCaseMethodFactory
$depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend)); $depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend));
$this->attributes[] = new Attribute( $this->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Depends::class, Depends::class,
[$depend], [$depend],
); );
} }

View File

@ -140,7 +140,7 @@ if (! function_exists('test')) {
*/ */
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
{ {
if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) { if ($description === null && TestSuite::getInstance()->test instanceof TestCase) {
return new HigherOrderTapProxy(TestSuite::getInstance()->test); return new HigherOrderTapProxy(TestSuite::getInstance()->test);
} }
@ -236,7 +236,7 @@ if (! function_exists('covers')) {
/** @var MutationTestRunner $runner */ /** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class); $runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */ /** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
@ -263,7 +263,7 @@ if (! function_exists('mutates')) {
/** @var MutationTestRunner $runner */ /** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class); $runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */ /** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
@ -320,7 +320,7 @@ if (! function_exists('visit')) {
*/ */
function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage
{ {
if (! class_exists(\Pest\Browser\Configuration::class)) { if (! class_exists(Pest\Browser\Configuration::class)) {
PluginBrowser::install(); PluginBrowser::install();
exit(0); exit(0);

View File

@ -71,7 +71,7 @@ final readonly class Kernel
$output, $output,
); );
register_shutdown_function(fn () => $kernel->shutdown()); register_shutdown_function($kernel->shutdown(...));
foreach (self::BOOTSTRAPPERS as $bootstrapper) { foreach (self::BOOTSTRAPPERS as $bootstrapper) {
$bootstrapper = Container::getInstance()->get($bootstrapper); $bootstrapper = Container::getInstance()->get($bootstrapper);

View File

@ -131,7 +131,7 @@ final readonly class Converter
// clean the paths of each frame. // clean the paths of each frame.
$frames = array_map( $frames = array_map(
fn (string $frame): string => $this->toRelativePath($frame), $this->toRelativePath(...),
$frames $frames
); );
@ -151,7 +151,7 @@ final readonly class Converter
{ {
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$firstTest = $this->getFirstTest($testSuite); $firstTest = $this->getFirstTest($testSuite);
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) { if ($firstTest instanceof TestMethod) {
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest); return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
} }
} }
@ -179,7 +179,7 @@ final readonly class Converter
public function getTestSuiteLocation(TestSuite $testSuite): ?string public function getTestSuiteLocation(TestSuite $testSuite): ?string
{ {
$firstTest = $this->getFirstTest($testSuite); $firstTest = $this->getFirstTest($testSuite);
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) { if (! $firstTest instanceof TestMethod) {
return null; return null;
} }
$path = $firstTest->testDox()->prettifiedClassName(); $path = $firstTest->testDox()->prettifiedClassName();

View File

@ -200,7 +200,7 @@ final class TeamCityLogger
public function testFinished(Finished $event): void public function testFinished(Finished $event): void
{ {
if (! $this->time instanceof \PHPUnit\Event\Telemetry\HRTime) { if (! $this->time instanceof HRTime) {
throw ShouldNotHappen::fromMessage('Start time has not been set.'); throw ShouldNotHappen::fromMessage('Start time has not been set.');
} }

View File

@ -9,6 +9,7 @@ use Closure;
use Countable; use Countable;
use DateTimeInterface; use DateTimeInterface;
use Error; use Error;
use Illuminate\Testing\TestResponse;
use InvalidArgumentException; use InvalidArgumentException;
use JsonSerializable; use JsonSerializable;
use Pest\Exceptions\InvalidExpectationValue; use Pest\Exceptions\InvalidExpectationValue;
@ -842,7 +843,7 @@ final class Expectation
is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(), is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(),
is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(),
is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(), is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(),
$this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line $this->value instanceof TestResponse => $this->value->getContent(), // @phpstan-ignore-line
is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
$this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), $this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
$this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), $this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
@ -983,7 +984,7 @@ final class Expectation
*/ */
private function export(mixed $value): string private function export(mixed $value): string
{ {
if (! $this->exporter instanceof \Pest\Support\Exporter) { if (! $this->exporter instanceof Exporter) {
$this->exporter = Exporter::default(); $this->exporter = Exporter::default();
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Pest\PendingCalls\Concerns; namespace Pest\PendingCalls\Concerns;
use Pest\Support\Description;
/** /**
* @internal * @internal
*/ */
@ -12,14 +14,14 @@ trait Describable
/** /**
* Note: this is property is not used; however, it gets added automatically by rector php. * Note: this is property is not used; however, it gets added automatically by rector php.
* *
* @var array<int, \Pest\Support\Description> * @var array<int, Description>
*/ */
public array $__describing; public array $__describing;
/** /**
* The describing of the test case. * The describing of the test case.
* *
* @var array<int, \Pest\Support\Description> * @var array<int, Description>
*/ */
public array $describing = []; public array $describing = [];
} }

View File

@ -73,7 +73,7 @@ final class DescribeCall
{ {
$filename = Backtrace::file(); $filename = Backtrace::file();
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) { if (! $this->currentBeforeEachCall instanceof BeforeEachCall) {
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); $this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
$this->currentBeforeEachCall->describing[] = $this->description; $this->currentBeforeEachCall->describing[] = $this->description;

View File

@ -22,6 +22,10 @@ use Pest\Support\NullClosure;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use PHPUnit\Framework\Attributes\CoversTrait;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -211,7 +215,7 @@ final class TestCall // @phpstan-ignore-line
{ {
foreach ($groups as $group) { foreach ($groups as $group) {
$this->testCaseMethod->attributes[] = new Attribute( $this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Group::class, Group::class,
[$group], [$group],
); );
} }
@ -604,7 +608,7 @@ final class TestCall // @phpstan-ignore-line
{ {
foreach ($classes as $class) { foreach ($classes as $class) {
$this->testCaseFactoryAttributes[] = new Attribute( $this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversClass::class, CoversClass::class,
[$class], [$class],
); );
} }
@ -627,7 +631,7 @@ final class TestCall // @phpstan-ignore-line
{ {
foreach ($traits as $trait) { foreach ($traits as $trait) {
$this->testCaseFactoryAttributes[] = new Attribute( $this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversTrait::class, CoversTrait::class,
[$trait], [$trait],
); );
} }
@ -650,7 +654,7 @@ final class TestCall // @phpstan-ignore-line
{ {
foreach ($functions as $function) { foreach ($functions as $function) {
$this->testCaseFactoryAttributes[] = new Attribute( $this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversFunction::class, CoversFunction::class,
[$function], [$function],
); );
} }

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string function version(): string
{ {
return '4.1.1'; return '4.4.2';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string

View File

@ -99,6 +99,7 @@ final readonly class Help implements HandlesArguments
{ {
$helpReflection = new PHPUnitHelp; $helpReflection = new PHPUnitHelp;
// @phpstan-ignore-next-line
$content = (fn (): array => $this->elements())->call($helpReflection); $content = (fn (): array => $this->elements())->call($helpReflection);
$content['Configuration'] = [...[[ $content['Configuration'] = [...[[
@ -141,6 +142,9 @@ final readonly class Help implements HandlesArguments
], [ ], [
'arg' => '--retry', 'arg' => '--retry',
'desc' => 'Run non-passing tests first and stop execution upon first error or failure', 'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
], [
'arg' => '--dirty',
'desc' => 'Only run tests that have uncommitted changes according to Git',
], ...$content['Selection']]; ], ...$content['Selection']];
$content['Reporting'] = [...$content['Reporting'], ...[ $content['Reporting'] = [...$content['Reporting'], ...[

View File

@ -7,6 +7,7 @@ namespace Pest\Plugins\Parallel\Handlers;
use Closure; use Closure;
use Composer\InstalledVersions; use Composer\InstalledVersions;
use Illuminate\Testing\ParallelRunner; use Illuminate\Testing\ParallelRunner;
use Orchestra\Testbench\TestCase;
use ParaTest\Options; use ParaTest\Options;
use ParaTest\RunnerInterface; use ParaTest\RunnerInterface;
use Pest\Contracts\Plugins\HandlesArguments; use Pest\Contracts\Plugins\HandlesArguments;
@ -39,13 +40,13 @@ final class Laravel implements HandlesArguments
* Executes the given closure when running Laravel. * Executes the given closure when running Laravel.
* *
* @param array<int, string> $arguments * @param array<int, string> $arguments
* @param CLosure(array<int, string>): array<int, string> $closure * @param Closure(array<int, string>): array<int, string> $closure
* @return array<int, string> * @return array<int, string>
*/ */
private function whenUsingLaravel(array $arguments, Closure $closure): array private function whenUsingLaravel(array $arguments, Closure $closure): array
{ {
$isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false); $isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false);
$isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class); $isLaravelPackage = class_exists(TestCase::class);
if ($isLaravelApplication && ! $isLaravelPackage) { if ($isLaravelApplication && ! $isLaravelPackage) {
return $closure($arguments); return $closure($arguments);

View File

@ -131,6 +131,7 @@ final class WrapperRunner implements RunnerInterface
$parameters = $this->handleLaravelHerd($parameters); $parameters = $this->handleLaravelHerd($parameters);
$parameters[] = $wrapper; $parameters[] = $wrapper;
$parameters[] = '--test-directory='.TestSuite::getInstance()->testPath;
$this->parameters = $parameters; $this->parameters = $parameters;
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry; $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry;

View File

@ -19,14 +19,14 @@ final class Closure
*/ */
public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
{ {
if (! $closure instanceof \Closure) { if (! $closure instanceof BaseClosure) {
throw ShouldNotHappen::fromMessage('Could not bind null closure.'); throw ShouldNotHappen::fromMessage('Could not bind null closure.');
} }
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
$closure = BaseClosure::bind($closure, $newThis, $newScope); $closure = BaseClosure::bind($closure, $newThis, $newScope);
if (! $closure instanceof \Closure) { if (! $closure instanceof BaseClosure) {
throw ShouldNotHappen::fromMessage('Could not bind closure.'); throw ShouldNotHappen::fromMessage('Could not bind closure.');
} }

View File

@ -28,7 +28,7 @@ final class Container
*/ */
public static function getInstance(): self public static function getInstance(): self
{ {
if (! self::$instance instanceof \Pest\Support\Container) { if (! self::$instance instanceof Container) {
self::$instance = new self; self::$instance = new self;
} }

View File

@ -46,6 +46,7 @@ final readonly class HigherOrderCallables
*/ */
public function and(mixed $value): Expectation public function and(mixed $value): Expectation
{ {
// @phpstan-ignore-next-line
return $this->expect($value); return $this->expect($value);
} }

View File

@ -31,10 +31,8 @@ final class HigherOrderTapProxy
/** /**
* Dynamically pass properties gets to the target. * Dynamically pass properties gets to the target.
*
* @return mixed
*/ */
public function __get(string $property) public function __get(string $property): mixed
{ {
if (property_exists($this->target, $property)) { if (property_exists($this->target, $property)) {
return $this->target->{$property}; return $this->target->{$property};

View File

@ -8,6 +8,7 @@ use Closure;
use InvalidArgumentException; use InvalidArgumentException;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
use ReflectionClass; use ReflectionClass;
use ReflectionException; use ReflectionException;
use ReflectionFunction; use ReflectionFunction;
@ -66,7 +67,7 @@ final class Reflection
{ {
$test = TestSuite::getInstance()->test; $test = TestSuite::getInstance()->test;
if (! $test instanceof \PHPUnit\Framework\TestCase) { if (! $test instanceof TestCase) {
return self::bindCallable($callable); return self::bindCallable($callable);
} }
@ -221,7 +222,7 @@ final class Reflection
{ {
$getProperties = fn (ReflectionClass $reflectionClass): array => array_filter( $getProperties = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map( array_map(
fn (ReflectionProperty $property): \ReflectionProperty => $property, fn (ReflectionProperty $property): ReflectionProperty => $property,
$reflectionClass->getProperties(), $reflectionClass->getProperties(),
), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(), ), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(),
); );
@ -256,7 +257,7 @@ final class Reflection
{ {
$getMethods = fn (ReflectionClass $reflectionClass): array => array_filter( $getMethods = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map( array_map(
fn (ReflectionMethod $method): \ReflectionMethod => $method, fn (ReflectionMethod $method): ReflectionMethod => $method,
$reflectionClass->getMethods($filter), $reflectionClass->getMethods($filter),
), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(), ), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(),
); );

View File

@ -79,7 +79,7 @@ final class Str
return $subject; return $subject;
} }
return substr($subject, 0, $pos); return mb_substr($subject, 0, $pos);
} }
/** /**

View File

@ -1,5 +1,7 @@
<?php <?php
use Tests\TestCase;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Test Case | Test Case
@ -11,7 +13,7 @@
| |
*/ */
pest()->extend(Tests\TestCase::class) pest()->extend(TestCase::class)
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature'); ->in('Feature');

View File

@ -1,5 +1,7 @@
<?php <?php
use Tests\TestCase;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Test Case | Test Case
@ -11,7 +13,7 @@
| |
*/ */
pest()->extend(Tests\TestCase::class)->in('Feature'); pest()->extend(TestCase::class)->in('Feature');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -19,7 +19,7 @@ test('pass with dataset', function ($data) {
[$filename] = TestSuite::getInstance()->snapshots->get(); [$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots-external/') expect($filename)->toStartWith('tests/.pest/snapshots-external/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot(); ->and($this->snapshotable)->toMatchSnapshot();
})->with(['my-datas-set-value']); })->with(['my-datas-set-value']);
@ -29,7 +29,7 @@ describe('within describe', function () {
[$filename] = TestSuite::getInstance()->snapshots->get(); [$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots-external/') expect($filename)->toStartWith('tests/.pest/snapshots-external/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot(); ->and($this->snapshotable)->toMatchSnapshot();
}); });
})->with(['my-datas-set-value']); })->with(['my-datas-set-value']);

View File

@ -0,0 +1,7 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
Pest Testing Framework 4.1.1. Pest Testing Framework 4.4.2.
USAGE: pest <file> [options] USAGE: pest <file> [options]
@ -27,6 +27,8 @@
--pr .... Output to standard output tests with the given pull request number --pr .... Output to standard output tests with the given pull request number
--pull-request Output to standard output tests with the given pull request number (alias for --pr) --pull-request Output to standard output tests with the given pull request number (alias for --pr)
--retry Run non-passing tests first and stop execution upon first error or failure --retry Run non-passing tests first and stop execution upon first error or failure
--dirty ...... Only run tests that have uncommitted changes according to Git
--all .................... Ignore test selection from XML configuration file
--list-suites ................................... List available test suites --list-suites ................................... List available test suites
--testsuite [name] ......... Only run tests from the specified test suite(s) --testsuite [name] ......... Only run tests from the specified test suite(s)
--exclude-testsuite [name] .. Exclude tests from the specified test suite(s) --exclude-testsuite [name] .. Exclude tests from the specified test suite(s)
@ -138,6 +140,7 @@
--only-summary-for-coverage-text Option for code coverage report in text format: only show summary --only-summary-for-coverage-text Option for code coverage report in text format: only show summary
--show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files --show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files
--coverage-xml [dir] . Write code coverage report in XML format to directory --coverage-xml [dir] . Write code coverage report in XML format to directory
--exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format
--warm-coverage-cache ........................... Warm static analysis cache --warm-coverage-cache ........................... Warm static analysis cache
--coverage-filter [dir] ........... Include [dir] in code coverage reporting --coverage-filter [dir] ........... Include [dir] in code coverage reporting
--path-coverage .......... Report path coverage in addition to line coverage --path-coverage .......... Report path coverage in addition to line coverage

View File

@ -1,3 +1,3 @@
Pest Testing Framework 4.1.1. Pest Testing Framework 4.4.2.

View File

@ -1,15 +1,19 @@
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234'] ##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
##teamcity[testCount count='3' flowId='1234'] ##teamcity[testCount count='4' flowId='1234']
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234'] ##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234'] ##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234'] ##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234'] ##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234'] ##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
##teamcity[testStarted name='can pass with dataset with data set "@(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "@(true)"' flowId='1234'] ##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
##teamcity[testFinished name='can pass with dataset with data set "@(true)"' duration='100000' flowId='1234'] ##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234'] ##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234'] ##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
Tests: 3 passed (3 assertions) Tests: 4 passed (4 assertions)
Duration: 1.00s Duration: 1.00s

View File

@ -1782,4 +1782,4 @@
✓ pass with dataset with ('my-datas-set-value') ✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value')
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2814 assertions) Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2813 assertions)

View File

@ -13,3 +13,9 @@ test('can also pass', function () {
test('can pass with dataset', function ($value) { test('can pass with dataset', function ($value) {
expect($value)->toEqual(true); expect($value)->toEqual(true);
})->with([true]); })->with([true]);
describe('block', function () {
test('can pass with dataset in describe block', function ($number) {
expect($number)->toBeInt();
})->with([1]);
});

View File

@ -1,5 +1,7 @@
<?php <?php
use Pest\Plugin;
trait PluginTrait trait PluginTrait
{ {
public function assertPluginTraitGotRegistered(): void public function assertPluginTraitGotRegistered(): void
@ -16,8 +18,8 @@ trait SecondPluginTrait
} }
} }
Pest\Plugin::uses(PluginTrait::class); Plugin::uses(PluginTrait::class);
Pest\Plugin::uses(SecondPluginTrait::class); Plugin::uses(SecondPluginTrait::class);
function _assertThat() function _assertThat()
{ {

View File

@ -1,6 +1,6 @@
<?php <?php
$foo = new \stdClass; $foo = new stdClass;
$foo->bar = 0; $foo->bar = 0;
beforeAll(function () use ($foo) { beforeAll(function () use ($foo) {

View File

@ -17,7 +17,7 @@ it('adds coverage if --coverage exist', function () {
$arguments = $plugin->handleArguments(['--coverage']); $arguments = $plugin->handleArguments(['--coverage']);
expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()]) expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()])
->and($plugin->coverage)->toBeTrue(); ->and($plugin->coverage)->toBeTrue();
})->skip(! \Pest\Support\Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); })->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available');
it('adds coverage if --min exist', function () { it('adds coverage if --min exist', function () {
$plugin = new CoveragePlugin(new ConsoleOutput); $plugin = new CoveragePlugin(new ConsoleOutput);

View File

@ -75,7 +75,7 @@ test('pass with dataset', function ($data) {
[$filename] = TestSuite::getInstance()->snapshots->get(); [$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots/') expect($filename)->toStartWith('tests/.pest/snapshots/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot(); ->and($this->snapshotable)->toMatchSnapshot();
})->with(['my-datas-set-value']); })->with(['my-datas-set-value']);
@ -85,7 +85,7 @@ describe('within describe', function () {
[$filename] = TestSuite::getInstance()->snapshots->get(); [$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots/') expect($filename)->toStartWith('tests/.pest/snapshots/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot(); ->and($this->snapshotable)->toMatchSnapshot();
}); });
})->with(['my-datas-set-value']); })->with(['my-datas-set-value']);

View File

@ -39,7 +39,7 @@ it('allows to call underlying protected/private methods', function () {
it('throws error if method do not exist', function () { it('throws error if method do not exist', function () {
test()->foo(); test()->foo();
})->throws(\ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()'); })->throws(ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()');
it('can forward unexpected calls to any global function')->_assertThat(); it('can forward unexpected calls to any global function')->_assertThat();

View File

@ -1,7 +1,9 @@
<?php <?php
use PHPUnit\Framework\TestCase;
/** /**
* @return \PHPUnit\Framework\TestCase * @return TestCase
*/ */
function myAssertTrue($value) function myAssertTrue($value)
{ {

View File

@ -1,8 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use PHPUnit\Framework\TestCase;
class MyCustomClassTest extends PHPUnit\Framework\TestCase class MyCustomClassTest extends TestCase
{ {
public function assertTrueIsTrue() public function assertTrueIsTrue()
{ {

View File

@ -1,6 +1,8 @@
<?php <?php
pest()->use(Tests\CustomTestCase\CustomTestCase::class)->in(__DIR__); use Tests\CustomTestCase\CustomTestCase;
pest()->use(CustomTestCase::class)->in(__DIR__);
test('closure was bound to CustomTestCase', function () { test('closure was bound to CustomTestCase', function () {
$this->assertCustomTrue(); $this->assertCustomTrue();

View File

@ -1,5 +1,7 @@
<?php <?php
use PHPUnit\Framework\TestCase;
trait MyCustomTrait trait MyCustomTrait
{ {
public function assertFalseIsFalse() public function assertFalseIsFalse()
@ -8,7 +10,7 @@ trait MyCustomTrait
} }
} }
abstract class MyCustomClass extends PHPUnit\Framework\TestCase abstract class MyCustomClass extends TestCase
{ {
public function assertTrueIsTrue() public function assertTrueIsTrue()
{ {

View File

@ -1,7 +1,9 @@
<?php <?php
use Pest\Configuration\Printer;
it('creates a printer instance', function () { it('creates a printer instance', function () {
$theme = pest()->printer(); $theme = pest()->printer();
expect($theme)->toBeInstanceOf(Pest\Configuration\Printer::class); expect($theme)->toBeInstanceOf(Printer::class);
}); });

View File

@ -1,8 +1,10 @@
<?php <?php
use Symfony\Component\Process\Process;
test('collision', function (array $arguments) { test('collision', function (array $arguments) {
$output = function () use ($arguments) { $output = function () use ($arguments) {
$process = (new Symfony\Component\Process\Process( $process = (new Process(
array_merge(['php', 'bin/pest', 'tests/Fixtures/CollisionTest.php'], $arguments), array_merge(['php', 'bin/pest', 'tests/Fixtures/CollisionTest.php'], $arguments),
null, null,
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'COLLISION_TEST' => true] ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'COLLISION_TEST' => true]

View File

@ -1,8 +1,10 @@
<?php <?php
use Symfony\Component\Process\Process;
test('visual snapshot of help command output', function () { test('visual snapshot of help command output', function () {
$output = function () { $output = function () {
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest', '--help'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process = (new Process(['php', 'bin/pest', '--help'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true']));
$process->run(); $process->run();

View File

@ -36,8 +36,8 @@ test('junit output', function () use ($normalizedPath, $run) {
expect($result['testsuite']['@attributes']) expect($result['testsuite']['@attributes'])
->name->toBe('Tests\tests\SuccessOnly') ->name->toBe('Tests\tests\SuccessOnly')
->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php'))
->tests->toBe('3') ->tests->toBe('4')
->assertions->toBe('3') ->assertions->toBe('4')
->errors->toBe('0') ->errors->toBe('0')
->failures->toBe('0') ->failures->toBe('0')
->skipped->toBe('0'); ->skipped->toBe('0');

View File

@ -16,7 +16,7 @@ $run = function () {
test('parallel', function () use ($run) { test('parallel', function () use ($run) {
expect($run('--exclude-group=integration')) expect($run('--exclude-group=integration'))
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1178 passed (2790 assertions)') ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)')
->toContain('Parallel: 3 processes'); ->toContain('Parallel: 3 processes');
})->skipOnWindows(); })->skipOnWindows();

View File

@ -1,5 +1,7 @@
<?php <?php
use Symfony\Component\Process\Process;
test('visual snapshot of test suite on success', function () { test('visual snapshot of test suite on success', function () {
$testsPath = dirname(__DIR__); $testsPath = dirname(__DIR__);
$snapshot = implode(DIRECTORY_SEPARATOR, [ $snapshot = implode(DIRECTORY_SEPARATOR, [
@ -9,7 +11,7 @@ test('visual snapshot of test suite on success', function () {
]); ]);
$output = function () use ($testsPath) { $output = function () use ($testsPath) {
$process = (new Symfony\Component\Process\Process( $process = (new Process(
['php', 'bin/pest'], ['php', 'bin/pest'],
dirname($testsPath), dirname($testsPath),
['EXCLUDE' => 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], ['EXCLUDE' => 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'],

View File

@ -1,5 +1,7 @@
<?php <?php
use Symfony\Component\Process\Process;
function normalize_windows_os_output(string $text): string function normalize_windows_os_output(string $text): string
{ {
$text = str_replace('\r', '', $text); $text = str_replace('\r', '', $text);
@ -17,7 +19,7 @@ test('visual snapshot of team city', function (string $testFile) {
]); ]);
$output = function () use ($testsPath) { $output = function () use ($testsPath) {
$process = (new Symfony\Component\Process\Process( $process = (new Process(
['php', 'bin/pest', '--teamcity', $testsPath], ['php', 'bin/pest', '--teamcity', $testsPath],
dirname(__DIR__, levels: 2), dirname(__DIR__, levels: 2),
[ [

View File

@ -1,8 +1,10 @@
<?php <?php
use Symfony\Component\Process\Process;
test('visual snapshot of help command output', function () { test('visual snapshot of help command output', function () {
$output = function () { $output = function () {
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest', '--version'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process = (new Process(['php', 'bin/pest', '--version'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true']));
$process->run(); $process->run();