From d77040e6b1c89058e24810d57a74cd45501833ae Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 29 Jul 2026 16:37:46 +0200 Subject: [PATCH 1/2] Upgrade test suite to Pest 5 --- .claude/skills/pest-plugin-agent/SKILL.md | 246 +++ .claude/skills/pest-testing/SKILL.md | 59 +- .ddev/commands/host/tia | 27 + .ddev/config.yaml | 1 + .github/skills/pest-plugin-agent/SKILL.md | 246 +++ .github/skills/pest-testing/SKILL.md | 59 +- .github/workflows/lint.yml | 3 + .github/workflows/tests.yml | 2 +- .github/workflows/tia-baseline.yml | 64 + AGENTS.md | 51 +- CLAUDE.md | 51 +- boost.json | 4 +- composer.json | 23 +- composer.lock | 1732 +++++++++++------ phpstan.neon | 2 + rector.php | 14 + tests/Browser/AdminMediaLibraryTest.php | 4 +- tests/Feature/AdminArticleManagementTest.php | 9 +- tests/Feature/AdminEventManagementTest.php | 5 +- tests/Feature/AdminLocationManagementTest.php | 5 +- tests/Feature/AdminMediaManagementTest.php | 7 +- tests/Feature/AdminSeasonManagementTest.php | 5 +- tests/Feature/AdminUserManagementTest.php | 11 +- tests/Feature/ArticleModelTest.php | 7 +- tests/Feature/ContactTest.php | 3 +- tests/Feature/DevelopmentEventSeederTest.php | 39 +- tests/Feature/EventModelTest.php | 8 +- tests/Feature/LocationModelTest.php | 2 +- tests/Feature/MakeAdminUserCommandTest.php | 9 +- tests/Feature/MediaAssetModelTest.php | 13 +- tests/Feature/PartnerCatalogueTest.php | 11 +- tests/Feature/ProjectCatalogueTest.php | 7 +- tests/Feature/PublicEventPagesTest.php | 6 +- .../Feature/PublicGettingStartedPageTest.php | 5 +- tests/Feature/PublicProjectPagesTest.php | 8 +- tests/Feature/SeasonModelTest.php | 9 +- tests/Feature/Settings/ProfileUpdateTest.php | 7 +- tests/Pest.php | 4 + tests/TestCase.php | 16 + 39 files changed, 2056 insertions(+), 728 deletions(-) create mode 100644 .claude/skills/pest-plugin-agent/SKILL.md create mode 100755 .ddev/commands/host/tia create mode 100644 .github/skills/pest-plugin-agent/SKILL.md create mode 100644 .github/workflows/tia-baseline.yml create mode 100644 rector.php diff --git a/.claude/skills/pest-plugin-agent/SKILL.md b/.claude/skills/pest-plugin-agent/SKILL.md new file mode 100644 index 0000000..d315c23 --- /dev/null +++ b/.claude/skills/pest-plugin-agent/SKILL.md @@ -0,0 +1,246 @@ +--- +name: pest-plugin-agent +description: One-shot Pest verification CLI for Laravel and PHP agents. Use whenever the user wants to quickly check that a change actually works, including hitting a route, asserting a model relationship or factory, checking a queued job, mail, or notification fires, screenshotting a page, asserting visible content, testing a click or form submission, checking for JavaScript errors, asserting accessibility, doing visual regression, or testing responsive layouts. Triggers include "verify this works", "did my change break X", "screenshot the homepage", "check this route returns 200", "make sure the mail fires", "test the login form", "see if the page renders", "check it on mobile", "is the form working", or any one-off behavioral check on a Laravel app that does not warrant a permanent test file. Also use after any Blade, Livewire, CSS, or JS change to visually confirm the result. Load this skill FIRST — before any shell command or throwaway test — whenever the request is to verify something that works. Prefer `vendor/bin/pest --agent=''` (SINGLE outer quotes so nothing is escaped) over writing throwaway test files. +--- + +# pest-plugin-agent + +One-shot Pest verification for AI agents. Wrap any PHP snippet in `vendor/bin/pest --agent=""`. Pest creates a temporary test, runs it, and deletes it. The snippet lives inside `it('verify', function () { ... })`, so use Pest's expectation API and any helpers available in the test suite (`visit()`, `actingAs()`, `Mail::fake()`, factories, and so on). + +## The invocation pattern — SINGLE outer quotes + +Inline the snippet, wrapped in **single** quotes. Single quotes tell the shell to interpret nothing, so `$variables`, `\App\Models\User`, backticks, and `!` all reach PHP literally — **there is nothing to escape.** Use double quotes for PHP string literals inside the snippet: + +```bash +vendor/bin/pest --agent='$user = \App\Models\User::factory()->create(); visit("/login")->type("email", $user->email)->press("Log in")->assertPathIs("/dashboard");' +``` + +**Double outer quotes are the trap.** `--agent="…$user…"` makes the shell interpolate `$user` to an empty string before PHP ever sees it — this is exactly how a login-form check silently breaks. Never use double outer quotes, and never hand-escape `\$`. If you catch yourself typing `\$`, you're doing it wrong: switch to single outer quotes. + +The examples below show snippet *contents*; wrap each in single quotes after `--agent=` to run it. + +### Fallback for snippets containing an apostrophe + +The only character single quotes can't hold is a literal single quote — an apostrophe anywhere in the snippet (e.g. `->type("bio", "I'm here")`) terminates the outer shell quote, even though it sits inside PHP's own double quotes, because the shell doesn't understand PHP quoting. In that case, **Write** the snippet to a `.php` file (plain body statements, no `"` writes a temp file shaped like this: + +```php +assertSee('Welcome');`) before invoking. +- **Use `vendor/bin/pest`, never bare `pest`.** The bare command often is not on `PATH` and produces "command not found" instead of a real result. +- **Fully qualify every class name:** `\App\Models\User`, `\Illuminate\Support\Facades\Mail`, `\App\Notifications\WelcomeNotification`. The generated test has no `use` statements, so unqualified names throw `Class "User" not found`. +- **Use the documented browser API exactly.** Methods like `onMobile()` or `mobileView()` do not exist — the chain is `->on()->mobile()`, `->on()->iPhone14Pro()`, or `->resize(w, h)`. If a method is not shown in this skill, do not invent it. +- **Do not replace real tests with `--agent`.** This is a verification probe, not a way to skip writing tests. If the behavior is worth a regression guard, write a proper test file. +- **Do not paper over missing setup.** If a check fails because a factory, seeder, or migration is missing, stop and ask the user to add it. Do not bend `--agent` invocations into fixtures. +- **Manage screenshot churn.** Screenshots land in `tests/Browser/Screenshots/`. Delete throwaway smoke screenshots once you've eyeballed them; for design-review workflows, keep them in a gitignored folder under that directory if you'll reference them across runs. +- **Manage temp snippet files.** If you used the apostrophe fallback and Wrote a snippet `.php` file, delete it once the check has run — it is not a test file and should not linger. (This is the file *you* Write, not the internal temp test Pest generates and cleans up on its own.) + +## Backend verification + +Seed state with factories inside the snippet. Do not rely on existing data. Each block below is the snippet *contents*; run it wrapped in single quotes: `vendor/bin/pest --agent=''`. + +```php +$user = \App\Models\User::factory()->create(); +expect($user->exists)->toBeTrue(); +``` + +```php +$post = \App\Models\Post::factory()->create(); +expect($post->author)->not->toBeNull(); +``` + +```php +$user = \App\Models\User::factory()->create(); +$response = $this->actingAs($user)->get('/api/users'); +$response->assertStatus(200); +``` + +Mail, notifications, and queued jobs work via the standard fakes: + +```php +\Illuminate\Support\Facades\Mail::fake(); +\App\Models\User::factory()->create()->notify(new \App\Notifications\Welcome()); +\Illuminate\Support\Facades\Notification::assertSentTo(...); +``` + +### Seeding for pages that need data + +The in-memory test DB starts empty on every run, so visual review of feed/list/dashboard pages will render the empty state unless you seed first. Run a seeder inline before visiting: + +```php +$this->seed(\Database\Seeders\DemoSeeder::class); +visit('/')->screenshot(filename: 'home'); +``` + +If the page still looks empty after seeding, the seeder probably isn't writing to the same connection the test sees — check `phpunit.xml` for the test DB configuration. + +## Frontend and browser verification + +Browser features come from `pestphp/pest-plugin-browser`. Full API reference: https://pestphp.com/docs/browser-testing. If `visit()` is undefined, install it first: + +```bash +composer require pestphp/pest-plugin-browser --dev +npm install playwright@latest +npx playwright install +``` + +Use relative paths in `visit()`. Pest resolves them against the app URL. Always pass a descriptive `filename:` to screenshots so the file is easy to locate afterwards — without it, the file defaults to `it_verify.png` and gets overwritten on every run. After any Blade, Livewire, CSS, or JS change, reach for these to visually confirm the result. + +**Screenshot signature: `screenshot(bool $fullPage = true, ?string $filename = null)`.** First positional arg is `$fullPage`, not the filename. Passing a path as the first arg throws `Argument #1 ($fullPage) must be of type bool, string given`. Always use named args, and note that `fullPage: true` (the default) produces very tall captures on long pages — pass `fullPage: false` for above-the-fold review. + +```php +// ✓ named args +visit('/')->screenshot(filename: 'home'); // → tests/Browser/Screenshots/home.png +visit('/')->screenshot(fullPage: false, filename: 'home'); // viewport only + +// ✗ positional path — runtime TypeError +visit('/')->screenshot('/tmp/home.png'); +``` + +You cannot redirect screenshots to an arbitrary path — they always land in `tests/Browser/Screenshots/` with the given filename. + +### Smoke screenshots + +Screenshot API reference: https://pestphp.com/docs/browser-testing#screenshot + +```php +visit('/')->screenshot(filename: 'homepage'); +visit('/dashboard')->screenshot(filename: 'dashboard', fullPage: true); +visit('/')->screenshotElement('.hero', filename: 'hero-section'); +``` + +### Content and element assertions + +```php +visit('/')->assertSee('Welcome'); +visit('/login')->assertPresent('input[name=email]'); +visit('/')->assertVisible('.navbar'); +``` + +### Responsive checks + +Emulate a device or set an explicit viewport: + +```php +visit('/')->on()->mobile()->screenshot(filename: 'home-mobile'); +visit('/')->on()->iPhone14Pro()->screenshot(filename: 'home-iphone14pro'); +visit('/')->resize(375, 812)->screenshot(filename: 'home-375x812'); +``` + +`resize()` after `on()->mobile()` overrides the device's width — pick one. Use `on()->...()` for device emulation (user agent, touch, DPR), `resize()` for raw viewport sizing. + +### Interaction flows + +```php +visit('/')->click('Login')->assertPathIs('/login'); +visit('/contact')->type('email', 'test@example.com')->press('Send')->assertSee('Message sent'); +``` + +#### Debugging a `click()` timeout + +If `click()` times out, the clickable element matched by your text or selector was never found or never became actionable within the browser timeout — `click()` auto-waits for the element, it does not wait for a navigation. Don't reach for a longer wait. Split the chain, screenshot, and inspect where you actually are and whether the target exists: + +```php +$page = visit('/'); +$page->click('Open dashboard'); +$page->screenshot(filename: 'after-click'); +dump($page->script('location.href')); +``` + +### Waiting for SPA / Inertia transitions + +You rarely need an explicit wait. Every page assertion (`assertSee`, `assertPathIs`, `assertPresent`, `assertVisible`, …) **auto-waits** — it retries until the condition holds or the browser timeout elapses. So for Inertia, Livewire, or other client-rendered transitions, just assert the post-transition state directly and let it wait: + +```php +visit('/')->click('Open dashboard')->assertPathIs('/dashboard')->assertSee('Welcome'); +visit('/feed')->assertSee('Latest posts')->screenshot(filename: 'feed'); +visit('/feed')->assertPresent('[data-feed-loaded]')->screenshot(filename: 'feed'); +``` + +There is no `waitForLocation()` or `waitFor()` on the page, and `waitForText()` is a deprecated alias for `assertSee()` — reach for the auto-waiting assertions instead. If you genuinely need a fixed pause, use `wait($seconds)` with an explicit number (calling `wait()` with no argument blocks for a key press and will hang). + +### Reading values back from the page + +`$page->script('')` evaluates JavaScript in the page and returns the JSON-decoded result as `mixed` — useful for debugging: + +```php +$page = visit('/'); +dump($page->script('document.title')); +dump($page->script('location.href')); +``` + +### Health checks + +JavaScript errors, accessibility, and visual drift: + +```php +visit('/')->assertNoJavaScriptErrors(); +visit('/')->assertNoAccessibilityIssues(); +visit('/')->assertScreenshotMatches(); +``` + +## Combining browser and backend + +Drive the UI, then assert the side effect. Always assert a frontend signal first (`assertSee`, `assertPathIs`) so you know the action was processed before checking what it touched on the backend. + +```php +\Illuminate\Support\Facades\Mail::fake(); +visit('/contact')->type('email', 'test@example.com')->type('message', 'Hello')->press('Send')->assertSee('Message sent'); +\Illuminate\Support\Facades\Mail::assertSent(\App\Mail\ContactForm::class); +``` + +```php +\Illuminate\Support\Facades\Notification::fake(); +visit('/register')->type('name', 'John')->type('email', 'john@example.com')->type('password', 'password')->press('Register')->assertPathIs('/dashboard'); +\Illuminate\Support\Facades\Notification::assertSentTo(\App\Models\User::first(), \App\Notifications\WelcomeNotification::class); +``` + +```php +visit('/checkout')->type('card', '4242424242424242')->press('Pay')->assertSee('Transaction processed'); +expect(\App\Models\Order::count())->toBe(1); +``` + +## Database and RefreshDatabase + +If a check fails with "no such table" or similar, look in `tests/Pest.php` for a commented `RefreshDatabase` line, for example `// uses(RefreshDatabase::class)->in('Feature');`. + +**Do not silently uncomment it.** Ask the user first. If the project's test database is persistent (anything other than SQLite `:memory:`), enabling `RefreshDatabase` wipes it on every run. Confirm the test database is in-memory or otherwise expendable before flipping the switch. + +## Pitfalls + +- **`use` inside the snippet is invalid.** The code runs inside a closure body, so namespace imports must happen at file top, which you do not control. Always use fully qualified class names. +- **`__DIR__` and `__FILE__` resolve to `/tmp`**, not the tests folder. Do not read fixtures by relative path. Pass absolute paths or use `base_path()` and `storage_path()`. +- **Multiple `--agent` options are allowed, but they blur failure output.** Each `--agent` becomes its own temporary one-test file, and every failure reports the test name as `verify` — distinguishable only by the temp file path. Prefer one focused snippet per invocation; never batch unrelated checks into one snippet. +- **An empty snippet is an error.** `--agent=` or a whitespace-only value aborts the run with "requires a non-empty PHP code snippet" instead of silently passing. Passing `--agent` with no value at all fails the same way. +- **Traits cannot be added inline.** `RefreshDatabase`, `WithFaker`, and similar traits must be wired through `tests/Pest.php` `uses()`. The snippet inherits whatever is already configured. +- **Path-scoped hooks and groups do not carry over.** Classes and traits from `uses(...)->in('Feature')` are re-applied to the generated test, but `beforeEach`/`afterEach` hooks and groups attached via `uses()->beforeEach(...)->in(...)` are bound to the directory path and will not run for agent snippets. If required setup lives in such a hook, inline it at the top of the snippet. +- **Browser tests need a reachable app.** `visit('/foo')` hits the configured app URL, so make sure `php artisan serve` (or your usual dev server) is running, or the browser plugin's built-in server is configured. +- **Screenshots persist on failure too.** A failed assertion still leaves the PNG in `tests/Browser/Screenshots/`. Sweep them up regardless of outcome. Without `filename:`, they overwrite each other as `it_verify.png`. +- **Shell escaping only bites with double outer quotes.** Backticks, `!` (zsh history), and `$` are interpreted by the shell before PHP sees them *only inside double quotes*. Wrapping the whole snippet in SINGLE outer quotes (`--agent='...'`) disables all of it — nothing is escaped, and `$user` reaches PHP intact. If you ever find yourself typing `\$` or wrestling with quotes, you used double quotes by mistake; switch to single. The only exception is a literal apostrophe in the snippet, which needs the file + `"$(cat …)"` fallback. + +## When NOT to use + +- The behavior deserves a permanent regression guard. Write a real test file in `tests/Feature` or `tests/Browser` instead. +- The check needs more than roughly three statements or any helper function. Long shell-quoted snippets are painful to read and edit; write a real test file. +- The user is asking for a fix or refactor, not a verification. Use the appropriate edit and test workflow, not `--agent`. diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md index ab27161..79cd0bc 100644 --- a/.claude/skills/pest-testing/SKILL.md +++ b/.claude/skills/pest-testing/SKILL.md @@ -1,16 +1,16 @@ --- name: pest-testing -description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." +description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." license: MIT metadata: author: laravel --- -# Pest Testing 4 +# Pest Testing 5 ## Documentation -Use `search-docs` for detailed Pest 4 patterns and documentation. +Use `search-docs` for detailed Pest 5 patterns and documentation. ## Basic Usage @@ -46,6 +46,7 @@ it('is true', function () { - Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. - Run all tests: `php artisan test --compact`. - Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`. ## Assertions @@ -82,16 +83,58 @@ it('has emails', function (string $email) { ]); ``` -## Pest 4 Features +## Pest 5 Features | Feature | Purpose | |---------|---------| +| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes | +| Time-Balanced Sharding | Split tests across CI shards by execution time | +| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more | | Browser Testing | Full integration tests in real browsers | | Smoke Testing | Validate multiple pages quickly | | Visual Regression | Compare screenshots for visual changes | -| Test Sharding | Parallel CI runs | | Architecture Testing | Enforce code conventions | +### Tia (Test Impact Analysis) + +Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration: + + +```shell +./vendor/bin/pest --parallel --tia +``` + +- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches. +- Detects Laravel, Symfony, Livewire, and Inertia automatically. + +### New Validation Expectations + +Pest 5 ships eight new validation matchers, all supporting `.not` negation: + + +```php +expect('nuno@pestphp.com')->toBeEmail(); +expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid(); +expect('192.168.1.1')->toBeIpAddress(); +expect('00:1a:2b:3c:4d:5e')->toBeMacAddress(); +expect('example.com')->toBeHostname(); +expect('example.co.uk')->toBeDomain(); +expect('Zm9vYmFy')->toBeBase64(); +expect('deadbeef')->toBeHexadecimal(); +``` + +### Time-Balanced Sharding + +Distribute tests across CI shards by execution time rather than count: + + +```shell +./vendor/bin/pest --update-shards +./vendor/bin/pest --shard=1/4 +``` + +Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent. + ### Browser Test Example Browser tests run in real browsers for full integration testing: @@ -140,14 +183,8 @@ $pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); Capture and compare screenshots to detect visual changes. -### Test Sharding - -Split tests across parallel processes for faster CI runs. - ### Architecture Testing -Pest 4 includes architecture testing (from Pest 3): - ```php arch('controllers') diff --git a/.ddev/commands/host/tia b/.ddev/commands/host/tia new file mode 100755 index 0000000..2c2481a --- /dev/null +++ b/.ddev/commands/host/tia @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +## Description: Run Pest using the shared TIA baseline from GitHub Actions +## Usage: tia [pest options] +## Example: "ddev tia" + +set -euo pipefail + +if ! command -v gh >/dev/null 2>&1; then + echo 'GitHub CLI (gh) is required on the host: https://cli.github.com' + exit 1 +fi + +if ! gh auth status >/dev/null 2>&1; then + echo 'GitHub CLI is not authenticated. Run: gh auth login' + exit 1 +fi + +github_token="$(gh auth token)" +xdebug_was_enabled="$(ddev exec php -r 'echo extension_loaded("xdebug") ? "1" : "0";')" + +if [[ "${xdebug_was_enabled}" == "0" ]]; then + ddev xdebug on + trap 'ddev xdebug off >/dev/null' EXIT +fi + +ddev exec env GH_TOKEN="${github_token}" XDEBUG_MODE=coverage ./vendor/bin/pest --parallel --tia "$@" diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 154e83c..0c0cec1 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -16,6 +16,7 @@ web_environment: nodejs_version: "24" corepack_enable: false webimage_extra_packages: + - gh - libasound2t64 - libatk-bridge2.0-0t64 - libatk1.0-0t64 diff --git a/.github/skills/pest-plugin-agent/SKILL.md b/.github/skills/pest-plugin-agent/SKILL.md new file mode 100644 index 0000000..d315c23 --- /dev/null +++ b/.github/skills/pest-plugin-agent/SKILL.md @@ -0,0 +1,246 @@ +--- +name: pest-plugin-agent +description: One-shot Pest verification CLI for Laravel and PHP agents. Use whenever the user wants to quickly check that a change actually works, including hitting a route, asserting a model relationship or factory, checking a queued job, mail, or notification fires, screenshotting a page, asserting visible content, testing a click or form submission, checking for JavaScript errors, asserting accessibility, doing visual regression, or testing responsive layouts. Triggers include "verify this works", "did my change break X", "screenshot the homepage", "check this route returns 200", "make sure the mail fires", "test the login form", "see if the page renders", "check it on mobile", "is the form working", or any one-off behavioral check on a Laravel app that does not warrant a permanent test file. Also use after any Blade, Livewire, CSS, or JS change to visually confirm the result. Load this skill FIRST — before any shell command or throwaway test — whenever the request is to verify something that works. Prefer `vendor/bin/pest --agent=''` (SINGLE outer quotes so nothing is escaped) over writing throwaway test files. +--- + +# pest-plugin-agent + +One-shot Pest verification for AI agents. Wrap any PHP snippet in `vendor/bin/pest --agent=""`. Pest creates a temporary test, runs it, and deletes it. The snippet lives inside `it('verify', function () { ... })`, so use Pest's expectation API and any helpers available in the test suite (`visit()`, `actingAs()`, `Mail::fake()`, factories, and so on). + +## The invocation pattern — SINGLE outer quotes + +Inline the snippet, wrapped in **single** quotes. Single quotes tell the shell to interpret nothing, so `$variables`, `\App\Models\User`, backticks, and `!` all reach PHP literally — **there is nothing to escape.** Use double quotes for PHP string literals inside the snippet: + +```bash +vendor/bin/pest --agent='$user = \App\Models\User::factory()->create(); visit("/login")->type("email", $user->email)->press("Log in")->assertPathIs("/dashboard");' +``` + +**Double outer quotes are the trap.** `--agent="…$user…"` makes the shell interpolate `$user` to an empty string before PHP ever sees it — this is exactly how a login-form check silently breaks. Never use double outer quotes, and never hand-escape `\$`. If you catch yourself typing `\$`, you're doing it wrong: switch to single outer quotes. + +The examples below show snippet *contents*; wrap each in single quotes after `--agent=` to run it. + +### Fallback for snippets containing an apostrophe + +The only character single quotes can't hold is a literal single quote — an apostrophe anywhere in the snippet (e.g. `->type("bio", "I'm here")`) terminates the outer shell quote, even though it sits inside PHP's own double quotes, because the shell doesn't understand PHP quoting. In that case, **Write** the snippet to a `.php` file (plain body statements, no `"` writes a temp file shaped like this: + +```php +assertSee('Welcome');`) before invoking. +- **Use `vendor/bin/pest`, never bare `pest`.** The bare command often is not on `PATH` and produces "command not found" instead of a real result. +- **Fully qualify every class name:** `\App\Models\User`, `\Illuminate\Support\Facades\Mail`, `\App\Notifications\WelcomeNotification`. The generated test has no `use` statements, so unqualified names throw `Class "User" not found`. +- **Use the documented browser API exactly.** Methods like `onMobile()` or `mobileView()` do not exist — the chain is `->on()->mobile()`, `->on()->iPhone14Pro()`, or `->resize(w, h)`. If a method is not shown in this skill, do not invent it. +- **Do not replace real tests with `--agent`.** This is a verification probe, not a way to skip writing tests. If the behavior is worth a regression guard, write a proper test file. +- **Do not paper over missing setup.** If a check fails because a factory, seeder, or migration is missing, stop and ask the user to add it. Do not bend `--agent` invocations into fixtures. +- **Manage screenshot churn.** Screenshots land in `tests/Browser/Screenshots/`. Delete throwaway smoke screenshots once you've eyeballed them; for design-review workflows, keep them in a gitignored folder under that directory if you'll reference them across runs. +- **Manage temp snippet files.** If you used the apostrophe fallback and Wrote a snippet `.php` file, delete it once the check has run — it is not a test file and should not linger. (This is the file *you* Write, not the internal temp test Pest generates and cleans up on its own.) + +## Backend verification + +Seed state with factories inside the snippet. Do not rely on existing data. Each block below is the snippet *contents*; run it wrapped in single quotes: `vendor/bin/pest --agent=''`. + +```php +$user = \App\Models\User::factory()->create(); +expect($user->exists)->toBeTrue(); +``` + +```php +$post = \App\Models\Post::factory()->create(); +expect($post->author)->not->toBeNull(); +``` + +```php +$user = \App\Models\User::factory()->create(); +$response = $this->actingAs($user)->get('/api/users'); +$response->assertStatus(200); +``` + +Mail, notifications, and queued jobs work via the standard fakes: + +```php +\Illuminate\Support\Facades\Mail::fake(); +\App\Models\User::factory()->create()->notify(new \App\Notifications\Welcome()); +\Illuminate\Support\Facades\Notification::assertSentTo(...); +``` + +### Seeding for pages that need data + +The in-memory test DB starts empty on every run, so visual review of feed/list/dashboard pages will render the empty state unless you seed first. Run a seeder inline before visiting: + +```php +$this->seed(\Database\Seeders\DemoSeeder::class); +visit('/')->screenshot(filename: 'home'); +``` + +If the page still looks empty after seeding, the seeder probably isn't writing to the same connection the test sees — check `phpunit.xml` for the test DB configuration. + +## Frontend and browser verification + +Browser features come from `pestphp/pest-plugin-browser`. Full API reference: https://pestphp.com/docs/browser-testing. If `visit()` is undefined, install it first: + +```bash +composer require pestphp/pest-plugin-browser --dev +npm install playwright@latest +npx playwright install +``` + +Use relative paths in `visit()`. Pest resolves them against the app URL. Always pass a descriptive `filename:` to screenshots so the file is easy to locate afterwards — without it, the file defaults to `it_verify.png` and gets overwritten on every run. After any Blade, Livewire, CSS, or JS change, reach for these to visually confirm the result. + +**Screenshot signature: `screenshot(bool $fullPage = true, ?string $filename = null)`.** First positional arg is `$fullPage`, not the filename. Passing a path as the first arg throws `Argument #1 ($fullPage) must be of type bool, string given`. Always use named args, and note that `fullPage: true` (the default) produces very tall captures on long pages — pass `fullPage: false` for above-the-fold review. + +```php +// ✓ named args +visit('/')->screenshot(filename: 'home'); // → tests/Browser/Screenshots/home.png +visit('/')->screenshot(fullPage: false, filename: 'home'); // viewport only + +// ✗ positional path — runtime TypeError +visit('/')->screenshot('/tmp/home.png'); +``` + +You cannot redirect screenshots to an arbitrary path — they always land in `tests/Browser/Screenshots/` with the given filename. + +### Smoke screenshots + +Screenshot API reference: https://pestphp.com/docs/browser-testing#screenshot + +```php +visit('/')->screenshot(filename: 'homepage'); +visit('/dashboard')->screenshot(filename: 'dashboard', fullPage: true); +visit('/')->screenshotElement('.hero', filename: 'hero-section'); +``` + +### Content and element assertions + +```php +visit('/')->assertSee('Welcome'); +visit('/login')->assertPresent('input[name=email]'); +visit('/')->assertVisible('.navbar'); +``` + +### Responsive checks + +Emulate a device or set an explicit viewport: + +```php +visit('/')->on()->mobile()->screenshot(filename: 'home-mobile'); +visit('/')->on()->iPhone14Pro()->screenshot(filename: 'home-iphone14pro'); +visit('/')->resize(375, 812)->screenshot(filename: 'home-375x812'); +``` + +`resize()` after `on()->mobile()` overrides the device's width — pick one. Use `on()->...()` for device emulation (user agent, touch, DPR), `resize()` for raw viewport sizing. + +### Interaction flows + +```php +visit('/')->click('Login')->assertPathIs('/login'); +visit('/contact')->type('email', 'test@example.com')->press('Send')->assertSee('Message sent'); +``` + +#### Debugging a `click()` timeout + +If `click()` times out, the clickable element matched by your text or selector was never found or never became actionable within the browser timeout — `click()` auto-waits for the element, it does not wait for a navigation. Don't reach for a longer wait. Split the chain, screenshot, and inspect where you actually are and whether the target exists: + +```php +$page = visit('/'); +$page->click('Open dashboard'); +$page->screenshot(filename: 'after-click'); +dump($page->script('location.href')); +``` + +### Waiting for SPA / Inertia transitions + +You rarely need an explicit wait. Every page assertion (`assertSee`, `assertPathIs`, `assertPresent`, `assertVisible`, …) **auto-waits** — it retries until the condition holds or the browser timeout elapses. So for Inertia, Livewire, or other client-rendered transitions, just assert the post-transition state directly and let it wait: + +```php +visit('/')->click('Open dashboard')->assertPathIs('/dashboard')->assertSee('Welcome'); +visit('/feed')->assertSee('Latest posts')->screenshot(filename: 'feed'); +visit('/feed')->assertPresent('[data-feed-loaded]')->screenshot(filename: 'feed'); +``` + +There is no `waitForLocation()` or `waitFor()` on the page, and `waitForText()` is a deprecated alias for `assertSee()` — reach for the auto-waiting assertions instead. If you genuinely need a fixed pause, use `wait($seconds)` with an explicit number (calling `wait()` with no argument blocks for a key press and will hang). + +### Reading values back from the page + +`$page->script('')` evaluates JavaScript in the page and returns the JSON-decoded result as `mixed` — useful for debugging: + +```php +$page = visit('/'); +dump($page->script('document.title')); +dump($page->script('location.href')); +``` + +### Health checks + +JavaScript errors, accessibility, and visual drift: + +```php +visit('/')->assertNoJavaScriptErrors(); +visit('/')->assertNoAccessibilityIssues(); +visit('/')->assertScreenshotMatches(); +``` + +## Combining browser and backend + +Drive the UI, then assert the side effect. Always assert a frontend signal first (`assertSee`, `assertPathIs`) so you know the action was processed before checking what it touched on the backend. + +```php +\Illuminate\Support\Facades\Mail::fake(); +visit('/contact')->type('email', 'test@example.com')->type('message', 'Hello')->press('Send')->assertSee('Message sent'); +\Illuminate\Support\Facades\Mail::assertSent(\App\Mail\ContactForm::class); +``` + +```php +\Illuminate\Support\Facades\Notification::fake(); +visit('/register')->type('name', 'John')->type('email', 'john@example.com')->type('password', 'password')->press('Register')->assertPathIs('/dashboard'); +\Illuminate\Support\Facades\Notification::assertSentTo(\App\Models\User::first(), \App\Notifications\WelcomeNotification::class); +``` + +```php +visit('/checkout')->type('card', '4242424242424242')->press('Pay')->assertSee('Transaction processed'); +expect(\App\Models\Order::count())->toBe(1); +``` + +## Database and RefreshDatabase + +If a check fails with "no such table" or similar, look in `tests/Pest.php` for a commented `RefreshDatabase` line, for example `// uses(RefreshDatabase::class)->in('Feature');`. + +**Do not silently uncomment it.** Ask the user first. If the project's test database is persistent (anything other than SQLite `:memory:`), enabling `RefreshDatabase` wipes it on every run. Confirm the test database is in-memory or otherwise expendable before flipping the switch. + +## Pitfalls + +- **`use` inside the snippet is invalid.** The code runs inside a closure body, so namespace imports must happen at file top, which you do not control. Always use fully qualified class names. +- **`__DIR__` and `__FILE__` resolve to `/tmp`**, not the tests folder. Do not read fixtures by relative path. Pass absolute paths or use `base_path()` and `storage_path()`. +- **Multiple `--agent` options are allowed, but they blur failure output.** Each `--agent` becomes its own temporary one-test file, and every failure reports the test name as `verify` — distinguishable only by the temp file path. Prefer one focused snippet per invocation; never batch unrelated checks into one snippet. +- **An empty snippet is an error.** `--agent=` or a whitespace-only value aborts the run with "requires a non-empty PHP code snippet" instead of silently passing. Passing `--agent` with no value at all fails the same way. +- **Traits cannot be added inline.** `RefreshDatabase`, `WithFaker`, and similar traits must be wired through `tests/Pest.php` `uses()`. The snippet inherits whatever is already configured. +- **Path-scoped hooks and groups do not carry over.** Classes and traits from `uses(...)->in('Feature')` are re-applied to the generated test, but `beforeEach`/`afterEach` hooks and groups attached via `uses()->beforeEach(...)->in(...)` are bound to the directory path and will not run for agent snippets. If required setup lives in such a hook, inline it at the top of the snippet. +- **Browser tests need a reachable app.** `visit('/foo')` hits the configured app URL, so make sure `php artisan serve` (or your usual dev server) is running, or the browser plugin's built-in server is configured. +- **Screenshots persist on failure too.** A failed assertion still leaves the PNG in `tests/Browser/Screenshots/`. Sweep them up regardless of outcome. Without `filename:`, they overwrite each other as `it_verify.png`. +- **Shell escaping only bites with double outer quotes.** Backticks, `!` (zsh history), and `$` are interpreted by the shell before PHP sees them *only inside double quotes*. Wrapping the whole snippet in SINGLE outer quotes (`--agent='...'`) disables all of it — nothing is escaped, and `$user` reaches PHP intact. If you ever find yourself typing `\$` or wrestling with quotes, you used double quotes by mistake; switch to single. The only exception is a literal apostrophe in the snippet, which needs the file + `"$(cat …)"` fallback. + +## When NOT to use + +- The behavior deserves a permanent regression guard. Write a real test file in `tests/Feature` or `tests/Browser` instead. +- The check needs more than roughly three statements or any helper function. Long shell-quoted snippets are painful to read and edit; write a real test file. +- The user is asking for a fix or refactor, not a verification. Use the appropriate edit and test workflow, not `--agent`. diff --git a/.github/skills/pest-testing/SKILL.md b/.github/skills/pest-testing/SKILL.md index ab27161..79cd0bc 100644 --- a/.github/skills/pest-testing/SKILL.md +++ b/.github/skills/pest-testing/SKILL.md @@ -1,16 +1,16 @@ --- name: pest-testing -description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." +description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." license: MIT metadata: author: laravel --- -# Pest Testing 4 +# Pest Testing 5 ## Documentation -Use `search-docs` for detailed Pest 4 patterns and documentation. +Use `search-docs` for detailed Pest 5 patterns and documentation. ## Basic Usage @@ -46,6 +46,7 @@ it('is true', function () { - Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. - Run all tests: `php artisan test --compact`. - Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`. ## Assertions @@ -82,16 +83,58 @@ it('has emails', function (string $email) { ]); ``` -## Pest 4 Features +## Pest 5 Features | Feature | Purpose | |---------|---------| +| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes | +| Time-Balanced Sharding | Split tests across CI shards by execution time | +| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more | | Browser Testing | Full integration tests in real browsers | | Smoke Testing | Validate multiple pages quickly | | Visual Regression | Compare screenshots for visual changes | -| Test Sharding | Parallel CI runs | | Architecture Testing | Enforce code conventions | +### Tia (Test Impact Analysis) + +Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration: + + +```shell +./vendor/bin/pest --parallel --tia +``` + +- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches. +- Detects Laravel, Symfony, Livewire, and Inertia automatically. + +### New Validation Expectations + +Pest 5 ships eight new validation matchers, all supporting `.not` negation: + + +```php +expect('nuno@pestphp.com')->toBeEmail(); +expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid(); +expect('192.168.1.1')->toBeIpAddress(); +expect('00:1a:2b:3c:4d:5e')->toBeMacAddress(); +expect('example.com')->toBeHostname(); +expect('example.co.uk')->toBeDomain(); +expect('Zm9vYmFy')->toBeBase64(); +expect('deadbeef')->toBeHexadecimal(); +``` + +### Time-Balanced Sharding + +Distribute tests across CI shards by execution time rather than count: + + +```shell +./vendor/bin/pest --update-shards +./vendor/bin/pest --shard=1/4 +``` + +Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent. + ### Browser Test Example Browser tests run in real browsers for full integration testing: @@ -140,14 +183,8 @@ $pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); Capture and compare screenshots to detect visual changes. -### Test Sharding - -Split tests across parallel processes for faster CI runs. - ### Architecture Testing -Pest 4 includes architecture testing (from Pest 3): - ```php arch('controllers') diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b2d0c5e..9cf0f0f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,6 +38,9 @@ jobs: - name: Run Pint run: composer lint + - name: Check Test Refactoring + run: composer refactor:check + - name: Format Frontend run: npm run format diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12d1884..87922e5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,7 +79,7 @@ jobs: - name: Tests with Coverage if: matrix.suite == 'php' - run: php artisan test --compact --coverage --min=95 + run: ./vendor/bin/pest --compact --coverage --min=95 - name: Browser Tests if: matrix.suite == 'browser' diff --git a/.github/workflows/tia-baseline.yml b/.github/workflows/tia-baseline.yml new file mode 100644 index 0000000..91194d3 --- /dev/null +++ b/.github/workflows/tia-baseline.yml @@ -0,0 +1,64 @@ +name: TIA Baseline + +on: + push: + branches: + - main + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + baseline: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: '8.4' + tools: composer:v2 + coverage: xdebug + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: '22' + + - name: Install Node Dependencies + run: npm ci + + - name: Install PHP Dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: Copy Environment File + run: cp .env.example .env + + - name: Generate Application Key + run: php artisan key:generate + + - name: Build Assets + run: npm run build + + - name: Record TIA Baseline + run: ./vendor/bin/pest --parallel --tia --coverage --fresh + + - name: Resolve TIA Baseline Path + id: baseline + run: echo "path=$(./vendor/bin/pest --baseline)" >> "$GITHUB_OUTPUT" + + - name: Upload TIA Baseline + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pest-tia-baseline + path: ${{ steps.baseline.outputs.path }} + include-hidden-files: true + retention-days: 30 diff --git a/AGENTS.md b/AGENTS.md index a62a318..cc8becb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,8 +21,9 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 -- pestphp/pest (PEST) - v4 -- phpunit/phpunit (PHPUNIT) - v12 +- pestphp/pest (PEST) - v5 +- phpunit/phpunit (PHPUNIT) - v13 +- rector/rector (RECTOR) - v2 - @inertiajs/react (INERTIA_REACT) - v3 - react (REACT) - v19 - tailwindcss (TAILWINDCSS) - v4 @@ -204,4 +205,50 @@ Use Wayfinder to generate TypeScript functions for Laravel routes. Import from ` - IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns. +=== pestphp/pest-plugin-agent rules === + +## Pest Agent Plugin + +`vendor/bin/pest --agent=""` runs a one-off Pest assertion without creating a test file — the fastest way to verify that a change actually works (a route response, a model relationship, a rendered page, a form submission, mail firing, a screenshot, JavaScript errors, and so on). + +### ALWAYS load the skill first + +Whenever the user asks you to check, verify, confirm, or "make sure" something **works** — and it can be exercised on a route, page, form, model, job, mail, notification, or screenshot — you **MUST** load the **`pest-plugin-agent` skill before doing anything else**. Do not reach for a shell command, a throwaway test file, or manual reasoning first. This includes prompts like "verify the login form works", "did my change break X", "screenshot the homepage", "check this route returns 200", "make sure the mail fires", "is the form working", or any behavioral check after a Blade, Livewire, CSS, or JS change. Load the skill, then follow it exactly. + +### NEVER fight shell escaping — use SINGLE outer quotes + +Inline the snippet, but wrap it in **single** quotes, not double. Single quotes tell the shell to interpret nothing, so `$variables`, `\App\Models\User`, backticks, and `!` all pass through to PHP literally — **there is nothing to escape.** Use double quotes for PHP string literals inside: + +```bash +vendor/bin/pest --agent='$user = \App\Models\User::factory()->create(); visit("/login")->type("email", $user->email)->press("Log in")->assertPathIs("/dashboard");' +``` + +Double outer quotes are the trap the shell springs on you — `--agent="…$user…"` makes the shell interpolate `$user` to nothing. Never do that, and never hand-escape `\$`. + +The one thing single quotes can't contain is a literal single quote (an apostrophe in the PHP). Only then, fall back to a file: **Write** the snippet to a `.php` file (plain body statements — no `type("email", "test@example.com")->type("password", "password")->press("Log in")->assertPathIs("/dashboard");' +vendor/bin/pest --agent='visit("/")->on()->mobile()->screenshot(fullPage: false, filename: "home-mobile");' +``` + +For full usage — backend examples, browser testing, screenshots, responsive checks, combining frontend and backend assertions, RefreshDatabase guidance, and pitfalls — load the **`pest-plugin-agent` skill**. + diff --git a/CLAUDE.md b/CLAUDE.md index a62a318..cc8becb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,8 +21,9 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 -- pestphp/pest (PEST) - v4 -- phpunit/phpunit (PHPUNIT) - v12 +- pestphp/pest (PEST) - v5 +- phpunit/phpunit (PHPUNIT) - v13 +- rector/rector (RECTOR) - v2 - @inertiajs/react (INERTIA_REACT) - v3 - react (REACT) - v19 - tailwindcss (TAILWINDCSS) - v4 @@ -204,4 +205,50 @@ Use Wayfinder to generate TypeScript functions for Laravel routes. Import from ` - IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns. +=== pestphp/pest-plugin-agent rules === + +## Pest Agent Plugin + +`vendor/bin/pest --agent=""` runs a one-off Pest assertion without creating a test file — the fastest way to verify that a change actually works (a route response, a model relationship, a rendered page, a form submission, mail firing, a screenshot, JavaScript errors, and so on). + +### ALWAYS load the skill first + +Whenever the user asks you to check, verify, confirm, or "make sure" something **works** — and it can be exercised on a route, page, form, model, job, mail, notification, or screenshot — you **MUST** load the **`pest-plugin-agent` skill before doing anything else**. Do not reach for a shell command, a throwaway test file, or manual reasoning first. This includes prompts like "verify the login form works", "did my change break X", "screenshot the homepage", "check this route returns 200", "make sure the mail fires", "is the form working", or any behavioral check after a Blade, Livewire, CSS, or JS change. Load the skill, then follow it exactly. + +### NEVER fight shell escaping — use SINGLE outer quotes + +Inline the snippet, but wrap it in **single** quotes, not double. Single quotes tell the shell to interpret nothing, so `$variables`, `\App\Models\User`, backticks, and `!` all pass through to PHP literally — **there is nothing to escape.** Use double quotes for PHP string literals inside: + +```bash +vendor/bin/pest --agent='$user = \App\Models\User::factory()->create(); visit("/login")->type("email", $user->email)->press("Log in")->assertPathIs("/dashboard");' +``` + +Double outer quotes are the trap the shell springs on you — `--agent="…$user…"` makes the shell interpolate `$user` to nothing. Never do that, and never hand-escape `\$`. + +The one thing single quotes can't contain is a literal single quote (an apostrophe in the PHP). Only then, fall back to a file: **Write** the snippet to a `.php` file (plain body statements — no `type("email", "test@example.com")->type("password", "password")->press("Log in")->assertPathIs("/dashboard");' +vendor/bin/pest --agent='visit("/")->on()->mobile()->screenshot(fullPage: false, filename: "home-mobile");' +``` + +For full usage — backend examples, browser testing, screenshots, responsive checks, combining frontend and backend assertions, RefreshDatabase guidance, and pitfalls — load the **`pest-plugin-agent` skill**. + diff --git a/boost.json b/boost.json index 08e94fa..63a5c94 100644 --- a/boost.json +++ b/boost.json @@ -9,6 +9,7 @@ "mcp": true, "nightwatch": false, "packages": [ + "pestphp/pest-plugin-agent", "spatie/laravel-permission" ], "sail": false, @@ -19,6 +20,7 @@ "pest-testing", "inertia-react-development", "tailwindcss-development", - "laravel-permission-development" + "laravel-permission-development", + "pest-plugin-agent" ] } diff --git a/composer.json b/composer.json index 7bf2007..2909ceb 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,14 @@ "laravel/sail": "^1.53", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.9.3", - "pestphp/pest": "^4.7", - "pestphp/pest-plugin-browser": "^4.3", - "pestphp/pest-plugin-laravel": "^4.1" + "pestphp/pest": "^5.0", + "pestphp/pest-plugin-agent": "^5.0", + "pestphp/pest-plugin-browser": "^5.0", + "pestphp/pest-plugin-laravel": "^5.0", + "pestphp/pest-plugin-phpstan": "^5.0", + "pestphp/pest-plugin-rector": "^5.0", + "phpstan/phpstan": "^2.1", + "rector/rector": "^2.2" }, "autoload": { "psr-4": { @@ -71,6 +76,12 @@ "lint:check": [ "pint --parallel --test" ], + "refactor": [ + "rector process" + ], + "refactor:check": [ + "rector process --dry-run" + ], "ci:check": [ "Composer\\Config::disableProcessTimeout", "npm run lint:check", @@ -84,10 +95,12 @@ "test": [ "@php artisan config:clear --ansi", "@lint:check", + "@refactor:check", "@types:check", - "@php artisan test" + "@php vendor/bin/pest" ], - "test:browser": "@php artisan test --compact tests/Browser", + "test:tia": "@php vendor/bin/pest --parallel --tia", + "test:browser": "@php vendor/bin/pest --compact tests/Browser", "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" diff --git a/composer.lock b/composer.lock index ad44e1e..8aa39c4 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": "07a8983cd4533e8a59173d9531d4e482", + "content-hash": "732d30833616dbf048c417c648d398f5", "packages": [ { "name": "aws/aws-crt-php", @@ -1023,16 +1023,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.15.1", + "version": "7.15.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f" + "reference": "744101956d78b7c1384d0cbf379db13e859167bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", - "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", + "reference": "744101956d78b7c1384d0cbf379db13e859167bf", "shasum": "" }, "require": { @@ -1131,7 +1131,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.1" + "source": "https://github.com/guzzle/guzzle/tree/7.15.2" }, "funding": [ { @@ -1147,7 +1147,7 @@ "type": "tidelift" } ], - "time": "2026-07-18T11:23:11+00:00" + "time": "2026-07-26T23:23:20+00:00" }, { "name": "guzzlehttp/promises", @@ -1354,16 +1354,16 @@ }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.9", + "version": "v1.0.10", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176" + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d7580af6d3f8384325d9cd3e99b21c3ed1848176", - "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/f6c24c21f42b990e9a58912b332d0874df6ba839", + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839", "shasum": "" }, "require": { @@ -1420,7 +1420,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.9" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.10" }, "funding": [ { @@ -1436,7 +1436,7 @@ "type": "tidelift" } ], - "time": "2026-07-08T16:19:22+00:00" + "time": "2026-07-17T13:53:03+00:00" }, { "name": "inertiajs/inertia-laravel", @@ -1628,16 +1628,16 @@ }, { "name": "laravel/framework", - "version": "v13.19.0", + "version": "v13.23.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "514502b38e11bd676ecf83b271c9452cc7500f16" + "reference": "92a707229148e57f08a249211c8a5a194159c619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/514502b38e11bd676ecf83b271c9452cc7500f16", - "reference": "514502b38e11bd676ecf83b271c9452cc7500f16", + "url": "https://api.github.com/repos/laravel/framework/zipball/92a707229148e57f08a249211c8a5a194159c619", + "reference": "92a707229148e57f08a249211c8a5a194159c619", "shasum": "" }, "require": { @@ -1663,7 +1663,7 @@ "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", - "monolog/monolog": "^3.0", + "monolog/monolog": "^3.10", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.3", @@ -1716,6 +1716,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/image": "self.version", "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", @@ -1742,6 +1743,7 @@ "ext-gmp": "*", "fakerphp/faker": "^1.24", "guzzlehttp/psr7": "^2.9", + "intervention/image": "^4.0", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", @@ -1778,6 +1780,7 @@ "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "intervention/image": "Required to use the image processing features (^4.0).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", @@ -1848,7 +1851,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-07-07T14:13:33+00:00" + "time": "2026-07-27T14:48:58+00:00" }, { "name": "laravel/passkeys", @@ -1979,16 +1982,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.13", + "version": "v2.0.15", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", "shasum": "" }, "require": { @@ -2036,7 +2039,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-16T14:03:50+00:00" + "time": "2026-07-21T16:49:22+00:00" }, { "name": "laravel/tinker", @@ -2172,16 +2175,16 @@ }, { "name": "league/commonmark", - "version": "2.8.2", + "version": "2.8.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7", + "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7", "shasum": "" }, "require": { @@ -2203,8 +2206,8 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", "scrutinizer/ocular": "^1.8.1", "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", @@ -2275,7 +2278,7 @@ "type": "tidelift" } ], - "time": "2026-03-19T13:16:38+00:00" + "time": "2026-07-12T15:29:16+00:00" }, { "name": "league/config", @@ -2548,16 +2551,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.16.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", "shasum": "" }, "require": { @@ -2567,7 +2570,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" }, "type": "library", "autoload": { @@ -2588,7 +2591,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" }, "funding": [ { @@ -2600,7 +2603,7 @@ "type": "tidelift" } ], - "time": "2024-09-21T08:32:55+00:00" + "time": "2026-07-09T11:49:27+00:00" }, { "name": "league/uri", @@ -3033,16 +3036,16 @@ }, { "name": "nesbot/carbon", - "version": "3.13.0", + "version": "3.13.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", - "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/2937ad3d1d2c506fd2bc97d571438a95641f44e2", + "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2", "shasum": "" }, "require": { @@ -3134,7 +3137,7 @@ "type": "tidelift" } ], - "time": "2026-06-18T13:49:15+00:00" + "time": "2026-07-09T18:23:49+00:00" }, { "name": "nette/schema", @@ -3205,16 +3208,16 @@ }, { "name": "nette/utils", - "version": "v4.1.4", + "version": "v4.1.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", "shasum": "" }, "require": { @@ -3234,7 +3237,7 @@ }, "suggest": { "ext-gd": "to use Image", - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", @@ -3290,9 +3293,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.4" + "source": "https://github.com/nette/utils/tree/v4.1.5" }, - "time": "2026-05-11T20:49:54+00:00" + "time": "2026-07-17T23:02:45+00:00" }, { "name": "nikic/php-parser", @@ -5256,16 +5259,16 @@ }, { "name": "symfony/console", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d" + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", - "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", + "url": "https://api.github.com/repos/symfony/console/zipball/535e18a1b8925f6c01a55b171d157ab66c2ace15", + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15", "shasum": "" }, "require": { @@ -5332,7 +5335,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.1" + "source": "https://github.com/symfony/console/tree/v8.1.2" }, "funding": [ { @@ -5352,7 +5355,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T12:55:20+00:00" + "time": "2026-07-27T13:58:19+00:00" }, { "name": "symfony/css-selector", @@ -5496,16 +5499,16 @@ }, { "name": "symfony/error-handler", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + "reference": "dc98404be5e8c949815e23fee1928f5de4f3f5d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", - "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/dc98404be5e8c949815e23fee1928f5de4f3f5d3", + "reference": "dc98404be5e8c949815e23fee1928f5de4f3f5d3", "shasum": "" }, "require": { @@ -5553,7 +5556,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v8.1.0" + "source": "https://github.com/symfony/error-handler/tree/v8.1.2" }, "funding": [ { @@ -5573,20 +5576,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0" + "reference": "c14c05a9e6da7f5e375e6efc28952c7e7dbddffb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abd6c11dc468725d1627302ad10f6cd486e9e3d0", - "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c14c05a9e6da7f5e375e6efc28952c7e7dbddffb", + "reference": "c14c05a9e6da7f5e375e6efc28952c7e7dbddffb", "shasum": "" }, "require": { @@ -5639,7 +5642,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.2" }, "funding": [ { @@ -5659,7 +5662,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T12:28:30+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5882,16 +5885,16 @@ }, { "name": "symfony/http-foundation", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6a168c8fcee806b57ac020244da14293d1f9a883" + "reference": "9943adbf5a64e2951a8d9eb0485310d55624f0e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6a168c8fcee806b57ac020244da14293d1f9a883", - "reference": "6a168c8fcee806b57ac020244da14293d1f9a883", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9943adbf5a64e2951a8d9eb0485310d55624f0e8", + "reference": "9943adbf5a64e2951a8d9eb0485310d55624f0e8", "shasum": "" }, "require": { @@ -5939,7 +5942,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.1.1" + "source": "https://github.com/symfony/http-foundation/tree/v8.1.2" }, "funding": [ { @@ -5959,7 +5962,7 @@ "type": "tidelift" } ], - "time": "2026-06-12T08:43:41+00:00" + "time": "2026-07-29T07:22:54+00:00" }, { "name": "symfony/http-kernel", @@ -6072,16 +6075,16 @@ }, { "name": "symfony/mailer", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12" + "reference": "221c7f326ace1ac2baee8331d829d5b7f04f4d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/4fa583a7377f28d54e4de442fba76375b2e20a12", - "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12", + "url": "https://api.github.com/repos/symfony/mailer/zipball/221c7f326ace1ac2baee8331d829d5b7f04f4d53", + "reference": "221c7f326ace1ac2baee8331d829d5b7f04f4d53", "shasum": "" }, "require": { @@ -6128,7 +6131,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v8.1.1" + "source": "https://github.com/symfony/mailer/tree/v8.1.2" }, "funding": [ { @@ -6148,20 +6151,20 @@ "type": "tidelift" } ], - "time": "2026-06-16T12:55:20+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "symfony/mime", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664" + "reference": "75f4779d4ec2e13f24a3a7e5d0347c340c7ca627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664", - "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664", + "url": "https://api.github.com/repos/symfony/mime/zipball/75f4779d4ec2e13f24a3a7e5d0347c340c7ca627", + "reference": "75f4779d4ec2e13f24a3a7e5d0347c340c7ca627", "shasum": "" }, "require": { @@ -6214,7 +6217,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v8.1.0" + "source": "https://github.com/symfony/mime/tree/v8.1.2" }, "funding": [ { @@ -6234,7 +6237,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-29T08:00:47+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6321,16 +6324,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -6379,7 +6382,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -6399,7 +6402,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -6824,16 +6827,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { @@ -6880,7 +6883,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -6900,20 +6903,20 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/polyfill-php86", - "version": "v1.38.0", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php86.git", - "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad" + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", - "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/6bc356ed3d8dbfeea8f0de235e34d670704e880e", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e", "shasum": "" }, "require": { @@ -6960,7 +6963,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-php86/tree/v1.41.0" }, "funding": [ { @@ -6980,7 +6983,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T11:52:35+00:00" + "time": "2026-07-02T13:42:24+00:00" }, { "name": "symfony/polyfill-uuid", @@ -7299,16 +7302,16 @@ }, { "name": "symfony/routing", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + "reference": "1058d4e13bb81dd9a6f7565686df7e13b880cdbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "url": "https://api.github.com/repos/symfony/routing/zipball/1058d4e13bb81dd9a6f7565686df7e13b880cdbd", + "reference": "1058d4e13bb81dd9a6f7565686df7e13b880cdbd", "shasum": "" }, "require": { @@ -7355,7 +7358,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.1.0" + "source": "https://github.com/symfony/routing/tree/v8.1.2" }, "funding": [ { @@ -7375,7 +7378,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/serializer", @@ -7565,16 +7568,16 @@ }, { "name": "symfony/string", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", "shasum": "" }, "require": { @@ -7631,7 +7634,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.0" + "source": "https://github.com/symfony/string/tree/v8.1.2" }, "funding": [ { @@ -7651,7 +7654,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "symfony/translation", @@ -7990,16 +7993,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "40096a2515a979f3125c5c928603995b8664c62a" + "reference": "865103cf742a039f34645b971fc3ace308d6c167" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/40096a2515a979f3125c5c928603995b8664c62a", - "reference": "40096a2515a979f3125c5c928603995b8664c62a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/865103cf742a039f34645b971fc3ace308d6c167", + "reference": "865103cf742a039f34645b971fc3ace308d6c167", "shasum": "" }, "require": { @@ -8015,7 +8018,7 @@ "symfony/http-kernel": "^7.4|^8.0", "symfony/process": "^7.4|^8.0", "symfony/uid": "^7.4|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -8053,7 +8056,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.1" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.2" }, "funding": [ { @@ -8073,7 +8076,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T10:54:51+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -8515,16 +8518,16 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v3.1.2", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "2f3ebed5a4f663968a0590dbb7654a8b32cb63cb" + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/2f3ebed5a4f663968a0590dbb7654a8b32cb63cb", - "reference": "2f3ebed5a4f663968a0590dbb7654a8b32cb63cb", + "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a", "shasum": "" }, "require": { @@ -8584,7 +8587,7 @@ ], "support": { "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.2" + "source": "https://github.com/amphp/amp/tree/v3.1.3" }, "funding": [ { @@ -8592,7 +8595,7 @@ "type": "github" } ], - "time": "2026-06-21T13:59:44+00:00" + "time": "2026-07-19T17:59:20+00:00" }, { "name": "amphp/byte-stream", @@ -9219,16 +9222,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.6", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "10941bf38de5c585aa2407b2ec4265d806d4eef2" + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/10941bf38de5c585aa2407b2ec4265d806d4eef2", - "reference": "10941bf38de5c585aa2407b2ec4265d806d4eef2", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", "shasum": "" }, "require": { @@ -9274,7 +9277,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.6" + "source": "https://github.com/amphp/pipeline/tree/v1.2.7" }, "funding": [ { @@ -9282,7 +9285,7 @@ "type": "github" } ], - "time": "2026-06-27T16:15:40+00:00" + "time": "2026-07-26T14:50:43+00:00" }, { "name": "amphp/process", @@ -9745,16 +9748,16 @@ }, { "name": "brianium/paratest", - "version": "v7.20.0", + "version": "v7.23.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + "reference": "6a3adf37c2726fefb66e03cc93bb0e8f75d92134" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/6a3adf37c2726fefb66e03cc93bb0e8f75d92134", + "reference": "6a3adf37c2726fefb66e03cc93bb0e8f75d92134", "shasum": "" }, "require": { @@ -9764,25 +9767,25 @@ "ext-simplexml": "*", "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.3.0 || ~8.4.0 || ~8.5.0", - "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", - "phpunit/php-file-iterator": "^6.0.1 || ^7", - "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.14 || ^13.0.5", - "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.7 || ^8.0.7", - "symfony/process": "^7.4.5 || ^8.0.5" + "php": "~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^14.2.0", + "phpunit/php-file-iterator": "^7", + "phpunit/php-timer": "^9", + "phpunit/phpunit": "^13.2.0", + "sebastian/environment": "^9.3.2", + "symfony/console": "^7.4.8 || ^8.1.0", + "symfony/process": "^7.4.8 || ^8.1.0" }, "require-dev": { "doctrine/coding-standard": "^14.0.0", "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan": "^2.2.2", "phpstan/phpstan-deprecation-rules": "^2.0.4", "phpstan/phpstan-phpunit": "^2.0.16", - "phpstan/phpstan-strict-rules": "^2.0.10", - "symfony/filesystem": "^7.4.6 || ^8.0.6" + "phpstan/phpstan-strict-rules": "^2.0.11", + "symfony/filesystem": "^7.4.8 || ^8.1.0" }, "bin": [ "bin/paratest", @@ -9822,7 +9825,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.23.0" }, "funding": [ { @@ -9834,149 +9837,7 @@ "type": "paypal" } ], - "time": "2026-03-29T15:46:14+00:00" - }, - { - "name": "composer/pcre", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<2.2.2" - }, - "require-dev": { - "phpstan/phpstan": "^2", - "phpstan/phpstan-deprecation-rules": "^2", - "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.4.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2026-06-07T11:47:49+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", - "shasum": "" - }, - "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2026-06-05T10:56:52+00:00" }, { "name": "daverandom/libdns", @@ -11305,23 +11166,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.9.4", + "version": "v8.9.5", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.8" + "symfony/console": "^7.4.14 || ^8.1.1" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -11329,12 +11190,12 @@ }, "require-dev": { "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.6", - "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", - "laravel/pint": "^1.29.1", - "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", - "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" }, "type": "library", "extra": { @@ -11397,46 +11258,47 @@ "type": "patreon" } ], - "time": "2026-04-21T14:04:20+00:00" + "time": "2026-07-15T19:09:14+00:00" }, { "name": "pestphp/pest", - "version": "v4.7.5", + "version": "v5.0.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0" + "reference": "6b2cd358e8a9d6d1abb93804b70e1c659bbc411b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", - "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "url": "https://api.github.com/repos/pestphp/pest/zipball/6b2cd358e8a9d6d1abb93804b70e1c659bbc411b", + "reference": "6b2cd358e8a9d6d1abb93804b70e1c659bbc411b", "shasum": "" }, "require": { - "brianium/paratest": "^7.20.0", - "composer/xdebug-handler": "^3.0.5", - "nunomaduro/collision": "^8.9.4", + "brianium/paratest": "^7.23.0", + "nunomaduro/collision": "^8.9.5", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest-plugin": "^4.0.0", - "pestphp/pest-plugin-arch": "^4.0.2", - "pestphp/pest-plugin-mutate": "^4.0.1", - "pestphp/pest-plugin-profanity": "^4.2.1", - "php": "^8.3.0", - "phpunit/phpunit": "^12.5.30", - "symfony/process": "^7.4.13|^8.1.0" + "pestphp/pest-plugin": "^5.0.0", + "pestphp/pest-plugin-arch": "^5.0.0", + "pestphp/pest-plugin-mutate": "^5.0.0", + "pestphp/pest-plugin-profanity": "^5.0.0", + "php": "^8.4", + "phpunit/phpunit": "^13.2.4", + "symfony/process": "^8.1.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.30", + "phpunit/phpunit": ">13.2.4", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "mrpunyapal/peststan": "^0.2.11", - "pestphp/pest-dev-tools": "^4.1.0", - "pestphp/pest-plugin-browser": "^4.3.1", - "pestphp/pest-plugin-type-coverage": "^4.0.4", + "laravel/pao": "^1.1.2", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-browser": "^5.0.0", + "pestphp/pest-plugin-phpstan": "^5.0.0", + "pestphp/pest-plugin-rector": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0", "psy/psysh": "^0.12.24" }, "bin": [ @@ -11504,7 +11366,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.7.5" + "source": "https://github.com/pestphp/pest/tree/v5.0.1" }, "funding": [ { @@ -11516,34 +11378,34 @@ "type": "github" } ], - "time": "2026-07-06T17:06:29+00:00" + "time": "2026-07-28T17:28:46+00:00" }, { "name": "pestphp/pest-plugin", - "version": "v4.0.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", - "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/87283f41aafa2561b7618be53eab00f2d06f4140", + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "composer-runtime-api": "^2.2.2", - "php": "^8.3" + "php": "^8.4" }, "conflict": { - "pestphp/pest": "<4.0.0" + "pestphp/pest": "<5.0.0" }, "require-dev": { - "composer/composer": "^2.8.10", - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "composer/composer": "^2.10.2", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "composer-plugin", "extra": { @@ -11570,7 +11432,84 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + "source": "https://github.com/pestphp/pest-plugin/tree/v5.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-18T17:10:45+00:00" + }, + { + "name": "pestphp/pest-plugin-agent", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-agent.git", + "reference": "10eeeb9d96a815c0f41049663a2afe0c48760674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-agent/zipball/10eeeb9d96a815c0f41049663a2afe0c48760674", + "reference": "10eeeb9d96a815c0f41049663a2afe0c48760674", + "shasum": "" + }, + "require": { + "pestphp/pest": "^5.0.0", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-browser": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Agent\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Agent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "AI agent verification plugin for Pest", + "keywords": [ + "Agent", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-agent/tree/v5.0.0" }, "funding": [ { @@ -11586,30 +11525,33 @@ "type": "patreon" } ], - "time": "2025-08-20T12:35:58+00:00" + "time": "2026-07-21T07:48:55+00:00" }, { "name": "pestphp/pest-plugin-arch", - "version": "v4.0.2", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", - "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", "ta-tikoma/phpunit-architecture-test": "^0.8.7" }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, "require-dev": { - "pestphp/pest": "^4.4.6", - "pestphp/pest-dev-tools": "^4.1.0" + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -11644,7 +11586,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v5.0.0" }, "funding": [ { @@ -11656,41 +11598,42 @@ "type": "github" } ], - "time": "2026-04-10T17:20:19+00:00" + "time": "2026-07-21T07:48:55+00:00" }, { "name": "pestphp/pest-plugin-browser", - "version": "v4.3.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-browser.git", - "reference": "b6e76d3e4a2f81da9f050ec54be2a29b402287c4" + "reference": "cfe0fc085c2d1ea33d9dec6e014c486ba3e2f525" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/b6e76d3e4a2f81da9f050ec54be2a29b402287c4", - "reference": "b6e76d3e4a2f81da9f050ec54be2a29b402287c4", + "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/cfe0fc085c2d1ea33d9dec6e014c486ba3e2f525", + "reference": "cfe0fc085c2d1ea33d9dec6e014c486ba3e2f525", "shasum": "" }, "require": { - "amphp/amp": "^3.1.1", - "amphp/http-server": "^3.4.4", + "amphp/amp": "^3.1.2", + "amphp/http-server": "^3.4.6", "amphp/websocket-client": "^2.0.2", "ext-sockets": "*", - "pestphp/pest": "^4.4.5", - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", - "symfony/process": "^7.4.8|^8.0.5" + "pestphp/pest": "^5.0.0", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", + "symfony/process": "^8.1.0" + }, + "conflict": { + "pestphp/pest": "<5.0.0" }, "require-dev": { - "ext-pcntl": "*", - "ext-posix": "*", - "livewire/livewire": "^3.7.15", - "nunomaduro/collision": "^8.9.3", - "orchestra/testbench": "^10.11.0", - "pestphp/pest-dev-tools": "^4.1.0", - "pestphp/pest-plugin-laravel": "^4.1", - "pestphp/pest-plugin-type-coverage": "^4.0.4" + "livewire/livewire": "^4.3.3", + "nunomaduro/collision": "^8.9.5", + "orchestra/testbench": "^11.1", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-laravel": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0" }, "type": "library", "extra": { @@ -11723,7 +11666,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-browser/tree/v4.3.1" + "source": "https://github.com/pestphp/pest-plugin-browser/tree/v5.0.0" }, "funding": [ { @@ -11739,31 +11682,34 @@ "type": "patreon" } ], - "time": "2026-04-08T21:04:12+00:00" + "time": "2026-07-21T07:48:55+00:00" }, { "name": "pestphp/pest-plugin-laravel", - "version": "v4.1.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-laravel.git", - "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" + "reference": "37a988d2c880c3dcf631a41250e84f6a7b2b9307" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", - "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/37a988d2c880c3dcf631a41250e84f6a7b2b9307", + "reference": "37a988d2c880c3dcf631a41250e84f6a7b2b9307", "shasum": "" }, "require": { - "laravel/framework": "^11.45.2|^12.52.0|^13.0", - "pestphp/pest": "^4.4.1", - "php": "^8.3.0" + "laravel/framework": "^11.45.2|^12.25.0|^13.20.0", + "pestphp/pest": "^5.0.0", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0" }, "require-dev": { - "laravel/dusk": "^8.3.6", - "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", - "pestphp/pest-dev-tools": "^4.1.0" + "laravel/dusk": "^8.6.0", + "orchestra/testbench": "^9.13.0|^10.5.0|^11.1", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -11801,7 +11747,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v5.0.0" }, "funding": [ { @@ -11813,32 +11759,35 @@ "type": "github" } ], - "time": "2026-02-21T00:29:45+00:00" + "time": "2026-07-21T07:48:56+00:00" }, { "name": "pestphp/pest-plugin-mutate", - "version": "v4.0.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-mutate.git", - "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + "reference": "fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", - "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77", + "reference": "fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77", "shasum": "" }, "require": { - "nikic/php-parser": "^5.6.1", - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", + "nikic/php-parser": "^5.8.0", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", "psr/simple-cache": "^3.0.0" }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, "require-dev": { - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0" + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0" }, "type": "library", "autoload": { @@ -11873,7 +11822,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v5.0.0" }, "funding": [ { @@ -11889,81 +11838,259 @@ "type": "github" } ], - "time": "2025-08-21T20:19:25+00:00" + "time": "2026-07-21T07:48:56+00:00" }, { - "name": "pestphp/pest-plugin-profanity", - "version": "v4.2.1", + "name": "pestphp/pest-plugin-phpstan", + "version": "v5.0.0", "source": { "type": "git", - "url": "https://github.com/pestphp/pest-plugin-profanity.git", - "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + "url": "https://github.com/pestphp/pest-plugin-phpstan.git", + "reference": "a1a7f29f9882b74df013298fee455cf0ab0b7dd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", - "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "url": "https://api.github.com/repos/pestphp/pest-plugin-phpstan/zipball/a1a7f29f9882b74df013298fee455cf0ab0b7dd6", + "reference": "a1a7f29f9882b74df013298fee455cf0ab0b7dd6", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3" + "pestphp/pest": "^5.0.0", + "php": "^8.4", + "phpstan/phpstan": "^2.2.5" + }, + "conflict": { + "pestphp/pest": "<5.0.0" }, "require-dev": { - "faissaloux/pest-plugin-inside": "^1.9", - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "laravel/pao": "^1.1.2", + "laravel/pint": "^1.29.3", + "pestphp/pest-plugin-rector": "^5.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-strict-rules": "^2.0.12", + "rector/rector": "^2.5.7" }, - "type": "library", + "type": "phpstan-extension", "extra": { - "pest": { - "plugins": [ - "Pest\\Profanity\\Plugin" + "phpstan": { + "includes": [ + "extension.neon" ] } }, "autoload": { "psr-4": { - "Pest\\Profanity\\": "src/" + "Pest\\PHPStan\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "The Pest Profanity Plugin", + "authors": [ + { + "name": "Punyapal Shah", + "email": "mrpunyapal@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "PHPStan plugin for Pest", "keywords": [ + "PHPStan", "framework", "pest", "php", "plugin", - "profanity", "test", "testing", "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + "source": "https://github.com/pestphp/pest-plugin-phpstan/tree/v5.0.0" }, - "time": "2025-12-08T00:13:17+00:00" + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/mrpunyapal", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-28T14:27:06+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.4", + "name": "pestphp/pest-plugin-profanity", + "version": "v5.0.0", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9", + "reference": "3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.11", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v5.0.0" + }, + "time": "2026-07-21T07:48:56+00:00" + }, + { + "name": "pestphp/pest-plugin-rector", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-rector.git", + "reference": "fe353125b8a7dadaddb70d2981501a430973c0b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-rector/zipball/fe353125b8a7dadaddb70d2981501a430973c0b9", + "reference": "fe353125b8a7dadaddb70d2981501a430973c0b9", + "shasum": "" + }, + "require": { + "php": "^8.4", + "rector/rector": "^2.5.7", + "symplify/rule-doc-generator-contracts": "^11.2" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "laravel/pao": "^1.1.2", + "laravel/pint": "^1.29.3", + "pestphp/pest": "^5.0.0", + "pestphp/pest-plugin-phpstan": "^5.0", + "phpstan/phpstan": "^2.2.5", + "symplify/rule-doc-generator": "^12.2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Rector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Punyapal Shah", + "email": "mrpunyapal@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Rector plugin for Pest", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "rector", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-rector/tree/v5.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/mrpunyapal", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-28T14:27:10+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", @@ -12067,11 +12194,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.5", + "version": "2.2.6", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0", - "reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a6e9b5a9420f6109c091e87d82683bd1a80b87ed", + "reference": "a6e9b5a9420f6109c091e87d82683bd1a80b87ed", "shasum": "" }, "require": { @@ -12127,37 +12254,39 @@ "type": "github" } ], - "time": "2026-07-05T06:31:06+00:00" + "time": "2026-07-26T21:22:49+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.5.7", + "version": "14.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "186dab580576598076de6818596d12b61801880e" + "reference": "82f6e49ff224e2cde923d74425e583a883910783" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", - "reference": "186dab580576598076de6818596d12b61801880e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/82f6e49ff224e2cde923d74425e583a883910783", + "reference": "82f6e49ff224e2cde923d74425e583a883910783", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", + "ext-mbstring": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", - "php": ">=8.3", - "phpunit/php-text-template": "^5.0", - "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.1.2", - "sebastian/lines-of-code": "^4.0.1", - "sebastian/version": "^6.0", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.1", + "sebastian/version": "^7.0", "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.2.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -12166,7 +12295,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5.x-dev" + "dev-main": "14.2.x-dev" } }, "autoload": { @@ -12195,7 +12324,7 @@ "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/12.5.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.3" }, "funding": [ { @@ -12215,32 +12344,32 @@ "type": "tidelift" } ], - "time": "2026-06-01T13:24:19+00:00" + "time": "2026-07-06T15:04:02+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -12268,7 +12397,7 @@ "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/6.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0" }, "funding": [ { @@ -12288,28 +12417,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2026-02-06T04:33:26+00:00" }, { "name": "phpunit/php-invoker", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "suggest": { "ext-pcntl": "*" @@ -12317,7 +12446,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -12344,40 +12473,52 @@ "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/6.0.0" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" }, "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-invoker", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2026-02-06T04:34:47+00:00" }, { "name": "phpunit/php-text-template", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -12404,40 +12545,52 @@ "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/5.0.0" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" }, "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-text-template", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2026-02-06T04:36:37+00:00" }, { "name": "phpunit/php-timer", - "version": "8.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -12464,56 +12617,70 @@ "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/8.0.0" + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" }, "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-timer", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2026-02-06T04:37:53+00:00" }, { "name": "phpunit/phpunit", - "version": "12.5.30", + "version": "13.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" + "reference": "8f5180f4627fc1978be2f61d8d9979dbe37e0c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", - "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8f5180f4627fc1978be2f61d8d9979dbe37e0c10", + "reference": "8f5180f4627fc1978be2f61d8d9979dbe37e0c10", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "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.3", - "phpunit/php-code-coverage": "^12.5.7", - "phpunit/php-file-iterator": "^6.0.1", - "phpunit/php-invoker": "^6.0.0", - "phpunit/php-text-template": "^5.0.0", - "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.1", - "sebastian/comparator": "^7.1.8", - "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.1.2", - "sebastian/exporter": "^7.0.3", - "sebastian/global-state": "^8.0.3", - "sebastian/object-enumerator": "^7.0.0", - "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.4", - "sebastian/version": "^6.0.0", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.2.3", + "phpunit/php-file-iterator": "^7.0.0", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.0", + "sebastian/comparator": "^8.3.0", + "sebastian/diff": "^9.0", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.1.0", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.0.0", + "sebastian/recursion-context": "^8.0.0", + "sebastian/type": "^7.0.1", + "sebastian/version": "^7.0.0", "staabm/side-effects-detector": "^1.0.5" }, "bin": [ @@ -12522,7 +12689,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5-dev" + "dev-main": "13.2-dev" } }, "autoload": { @@ -12554,7 +12721,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.30" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.4" }, "funding": [ { @@ -12562,7 +12729,67 @@ "type": "other" } ], - "time": "2026-06-15T13:12:30+00:00" + "time": "2026-07-08T08:36:51+00:00" + }, + { + "name": "rector/rector", + "version": "2.5.8", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "861c77fd2a6d45317a401f4e0b617f947e8b6f96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/861c77fd2a6d45317a401f4e0b617f947e8b6f96", + "reference": "861c77fd2a6d45317a401f4e0b617f947e8b6f96", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.2.6" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.5.8" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-07-27T06:19:16+00:00" }, { "name": "revolt/event-loop", @@ -12638,28 +12865,28 @@ }, { "name": "sebastian/cli-parser", - "version": "4.2.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/48a4654fa5e48c1c81214e9930048a572d4b23ca", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -12683,7 +12910,7 @@ "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/4.2.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.0" }, "funding": [ { @@ -12703,31 +12930,31 @@ "type": "tidelift" } ], - "time": "2026-05-17T05:29:34+00:00" + "time": "2026-02-06T04:39:44+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.8", + "version": "8.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7c65c1e79836812819705b473a90c12399542485" + "reference": "c025fc7604afab3f195fab7cdaf72327331af241" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", - "reference": "7c65c1e79836812819705b473a90c12399542485", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c025fc7604afab3f195fab7cdaf72327331af241", + "reference": "c025fc7604afab3f195fab7cdaf72327331af241", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0.3" + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.1.0" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -12735,7 +12962,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.1-dev" + "dev-main": "8.3-dev" } }, "autoload": { @@ -12775,7 +13002,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/8.3.0" }, "funding": [ { @@ -12795,33 +13022,33 @@ "type": "tidelift" } ], - "time": "2026-05-21T04:45:25+00:00" + "time": "2026-06-05T03:06:45+00:00" }, { "name": "sebastian/complexity", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -12845,41 +13072,53 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" }, "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/complexity", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:25+00:00" + "time": "2026-02-06T04:41:32+00:00" }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a3fb6a298a265ff487a91bbea46e03cd01dbb226", + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^13.2", + "symfony/process": "^7.4.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -12912,35 +13151,47 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.0" }, "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/diff", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2026-06-05T03:04:51+00:00" }, { "name": "sebastian/environment", - "version": "8.1.2", + "version": "9.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.26" + "phpunit/phpunit": "^13.1.11" }, "suggest": { "ext-posix": "*" @@ -12948,7 +13199,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.1-dev" + "dev-main": "9.3-dev" } }, "autoload": { @@ -12976,7 +13227,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.2" }, "funding": [ { @@ -12996,34 +13247,34 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:40:20+00:00" + "time": "2026-05-25T13:41:38+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.3", + "version": "8.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -13066,7 +13317,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/8.1.1" }, "funding": [ { @@ -13086,35 +13337,173 @@ "type": "tidelift" } ], - "time": "2026-05-20T04:37:17+00:00" + "time": "2026-07-13T11:35:11+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.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 filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "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/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.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 describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "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/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.3", + "version": "9.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.1.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -13140,7 +13529,7 @@ "support": { "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/8.0.3" + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" }, "funding": [ { @@ -13160,33 +13549,33 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:10:33+00:00" + "time": "2026-06-01T15:11:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", "shasum": "" }, "require": { - "nikic/php-parser": "^5.7.0", - "php": ">=8.3" + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -13210,7 +13599,7 @@ "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/4.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" }, "funding": [ { @@ -13230,34 +13619,34 @@ "type": "tidelift" } ], - "time": "2026-05-19T16:22:07+00:00" + "time": "2026-07-09T08:42:34+00:00" }, { "name": "sebastian/object-enumerator", - "version": "7.0.0", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -13280,40 +13669,52 @@ "support": { "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/7.0.0" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0" }, "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/object-enumerator", + "type": "tidelift" } ], - "time": "2025-02-07T04:57:48+00:00" + "time": "2026-02-06T04:46:36+00:00" }, { "name": "sebastian/object-reflector", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -13336,40 +13737,52 @@ "support": { "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/5.0.0" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0" }, "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/object-reflector", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:17+00:00" + "time": "2026-02-06T04:47:13+00:00" }, { "name": "sebastian/recursion-context", - "version": "7.0.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/74c5af21f6a5833e91767ca068c4d3dfec15317e", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -13400,7 +13813,7 @@ "support": { "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/7.0.1" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.0" }, "funding": [ { @@ -13420,32 +13833,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:44:59+00:00" + "time": "2026-02-06T04:51:28+00:00" }, { "name": "sebastian/type", - "version": "6.0.4", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "82ff822c2edc46724be9f7411d3163021f602773" + "reference": "fee0309275847fefd7636167085e379c1dbf6990" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", - "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fee0309275847fefd7636167085e379c1dbf6990", + "reference": "fee0309275847fefd7636167085e379c1dbf6990", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.1.10" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -13469,7 +13882,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + "source": "https://github.com/sebastianbergmann/type/tree/7.0.1" }, "funding": [ { @@ -13489,29 +13902,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T06:45:45+00:00" + "time": "2026-05-20T06:49:11+00:00" }, { "name": "sebastian/version", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -13535,15 +13948,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, "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/version", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:38+00:00" + "time": "2026-02-06T04:52:52+00:00" }, { "name": "staabm/side-effects-detector", @@ -13673,6 +14098,65 @@ ], "time": "2026-06-09T11:06:24+00:00" }, + { + "name": "symplify/rule-doc-generator-contracts", + "version": "11.2.0", + "source": { + "type": "git", + "url": "https://github.com/symplify/rule-doc-generator-contracts.git", + "reference": "479cfcfd46047f80624aba931d9789e50475b5c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symplify/rule-doc-generator-contracts/zipball/479cfcfd46047f80624aba931d9789e50475b5c6", + "reference": "479cfcfd46047f80624aba931d9789e50475b5c6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.2", + "rector/rector": "^0.15.10", + "symplify/easy-ci": "^11.1", + "symplify/easy-coding-standard": "^11.1", + "symplify/easy-testing": "^11.1", + "symplify/phpstan-extensions": "^11.1", + "symplify/phpstan-rules": "11.2.3.72", + "tomasvotruba/unused-public": "^0.0.34" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symplify\\RuleDocGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Contracts for production code of RuleDocGenerator", + "support": { + "source": "https://github.com/symplify/rule-doc-generator-contracts/tree/11.2.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-03-18T22:02:54+00:00" + }, { "name": "ta-tikoma/phpunit-architecture-test", "version": "0.8.7", diff --git a/phpstan.neon b/phpstan.neon index 1ca23a4..70170ad 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ includes: - vendor/larastan/larastan/extension.neon - vendor/nesbot/carbon/extension.neon + - vendor/pestphp/pest-plugin-phpstan/extension.neon parameters: paths: @@ -9,5 +10,6 @@ parameters: - config/ - database/ - routes/ + - tests/ level: 7 diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..f2be7b8 --- /dev/null +++ b/rector.php @@ -0,0 +1,14 @@ +withPaths([ + __DIR__.'/tests', + ]) + ->withSets([ + PestSetList::CODING_STYLE, + ]); diff --git a/tests/Browser/AdminMediaLibraryTest.php b/tests/Browser/AdminMediaLibraryTest.php index 154cc76..1f56252 100644 --- a/tests/Browser/AdminMediaLibraryTest.php +++ b/tests/Browser/AdminMediaLibraryTest.php @@ -7,6 +7,8 @@ use App\Models\User; use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Support\Facades\Vite; +use Pest\Browser\Api\AwaitableWebpage; +use Pest\Browser\Api\Webpage; beforeEach(function () { Vite::useHotFile(storage_path('framework/testing/vite.hot')); @@ -274,7 +276,7 @@ ->assertNoJavaScriptErrors(); }); -function selectMediaUploadImage($page): void +function selectMediaUploadImage(AwaitableWebpage|Webpage $page): void { $page->script(<<<'JS' async () => { diff --git a/tests/Feature/AdminArticleManagementTest.php b/tests/Feature/AdminArticleManagementTest.php index 930c50c..cb2a84a 100644 --- a/tests/Feature/AdminArticleManagementTest.php +++ b/tests/Feature/AdminArticleManagementTest.php @@ -150,8 +150,8 @@ expect($article) ->title->toBe('Nieuw seizoen van start') - ->status->toBe(ArticleStatus::Published); - expect($article->published_at)->not->toBeNull(); + ->status->toBe(ArticleStatus::Published) + ->and($article->published_at)->not->toBeNull(); }); test('article requests reject missing titles and invalid categories', function () { @@ -182,7 +182,10 @@ $this->assertModelMissing($article); }); -/** @param array $overrides */ +/** + * @param array $overrides + * @return array + */ function validArticlePayload(array $overrides = []): array { return [ diff --git a/tests/Feature/AdminEventManagementTest.php b/tests/Feature/AdminEventManagementTest.php index b1fabf1..cde0cc7 100644 --- a/tests/Feature/AdminEventManagementTest.php +++ b/tests/Feature/AdminEventManagementTest.php @@ -520,7 +520,10 @@ $this->assertModelMissing($event); }); -/** @param array $overrides */ +/** + * @param array $overrides + * @return array + */ function validEventPayload(Location $location, array $overrides = []): array { return [ diff --git a/tests/Feature/AdminLocationManagementTest.php b/tests/Feature/AdminLocationManagementTest.php index eba29e4..a4cd5ff 100644 --- a/tests/Feature/AdminLocationManagementTest.php +++ b/tests/Feature/AdminLocationManagementTest.php @@ -204,7 +204,10 @@ $this->assertModelExists($location); }); -/** @param array $overrides */ +/** + * @param array $overrides + * @return array + */ function validLocationPayload(array $overrides = []): array { return [ diff --git a/tests/Feature/AdminMediaManagementTest.php b/tests/Feature/AdminMediaManagementTest.php index e930cf8..7b5b1b6 100644 --- a/tests/Feature/AdminMediaManagementTest.php +++ b/tests/Feature/AdminMediaManagementTest.php @@ -85,7 +85,7 @@ public function __construct(string $path) parent::__construct($path, 'missing-image.jpg', 'image/jpeg', null, true); } - public function getMimeType(): ?string + public function getMimeType(): string { return 'image/jpeg'; } @@ -353,7 +353,10 @@ public function getRealPath(): string|false ]))->assertSessionHasErrors('cover_image_id'); }); -/** @return array */ +/** + * @param array $overrides + * @return array + */ function eventPayload(Location $location, array $overrides = []): array { return [ diff --git a/tests/Feature/AdminSeasonManagementTest.php b/tests/Feature/AdminSeasonManagementTest.php index aaeeb84..3f631da 100644 --- a/tests/Feature/AdminSeasonManagementTest.php +++ b/tests/Feature/AdminSeasonManagementTest.php @@ -281,7 +281,10 @@ $this->assertModelMissing($ticket); }); -/** @param array $overrides */ +/** + * @param array $overrides + * @return array + */ function validSeasonPayload(array $overrides = []): array { return [ diff --git a/tests/Feature/AdminUserManagementTest.php b/tests/Feature/AdminUserManagementTest.php index 4a804c7..d6203d6 100644 --- a/tests/Feature/AdminUserManagementTest.php +++ b/tests/Feature/AdminUserManagementTest.php @@ -296,13 +296,16 @@ $this->assertGuest(); }); -/** @param array $overrides */ +/** + * @param array $overrides + * @return array + */ function validUserPayload(array $overrides = [], ?User $user = null): array { return [ - 'name' => $user?->name ?? 'Platformgebruiker', - 'email' => $user?->email ?? 'platform@example.com', - 'locale' => $user?->locale ?? 'en', + 'name' => $user instanceof User ? $user->name : 'Platformgebruiker', + 'email' => $user instanceof User ? $user->email : 'platform@example.com', + 'locale' => $user instanceof User ? $user->locale : 'en', 'roles' => [Role::Editor->value], ...$overrides, ]; diff --git a/tests/Feature/ArticleModelTest.php b/tests/Feature/ArticleModelTest.php index 9e0c16a..653b809 100644 --- a/tests/Feature/ArticleModelTest.php +++ b/tests/Feature/ArticleModelTest.php @@ -35,10 +35,9 @@ ->published_at->toBeInstanceOf(CarbonImmutable::class) ->status->toBe(ArticleStatus::Published) ->category->toBe(ArticleCategory::RaceReport) - ->author->id->toBe($author->id) - ->coverImage->id->toBe($coverImage->id); - - expect($author->articles()->whereKey($article->id)->exists())->toBeTrue(); + ->and($article->author?->id)->toBe($author->id) + ->and($article->coverImage?->id)->toBe($coverImage->id) + ->and($author->articles()->whereKey($article->id)->exists())->toBeTrue(); }); test('article enum values are enforced by the database', function (string $column) { diff --git a/tests/Feature/ContactTest.php b/tests/Feature/ContactTest.php index 297638b..a3408a6 100644 --- a/tests/Feature/ContactTest.php +++ b/tests/Feature/ContactTest.php @@ -57,7 +57,8 @@ ->source_context->toBe('partners-page') ->delivery_status->toBe(ContactDeliveryStatus::Pending) ->consented_at->not->toBeNull() - ->delivered_at->toBeNull(); + ->delivered_at->toBeNull() + ->and($contactSubmission->email)->toBeEmail(); Notification::assertSentTo( $administrator, diff --git a/tests/Feature/DevelopmentEventSeederTest.php b/tests/Feature/DevelopmentEventSeederTest.php index ed7774e..1bc8a7c 100644 --- a/tests/Feature/DevelopmentEventSeederTest.php +++ b/tests/Feature/DevelopmentEventSeederTest.php @@ -24,14 +24,14 @@ }); test('the local demo event command creates the same representative dataset on repeated runs', function () { - $this->artisan('dds:seed-demo-events') + $this->pendingArtisan('dds:seed-demo-events') ->expectsOutput('10 demo-events zijn aangemaakt of bijgewerkt.') ->assertSuccessful(); $firstRun = demoEventsSnapshot(); $firstIds = demoRecordIds(); - $this->artisan('dds:seed-demo-events')->assertSuccessful(); + $this->pendingArtisan('dds:seed-demo-events')->assertSuccessful(); $events = Event::query() ->whereIn('slug', DevelopmentEventSeeder::EVENT_SLUGS) @@ -112,7 +112,7 @@ CarbonImmutable::parse('2027-12-20 10:00:00', 'Europe/Amsterdam'), ); - $this->artisan('dds:seed-demo-events')->assertSuccessful(); + $this->pendingArtisan('dds:seed-demo-events')->assertSuccessful(); $firstEvent = Event::query() ->where('slug', DevelopmentEventSeeder::EVENT_SLUGS[0]) @@ -133,7 +133,7 @@ }); test('the dataset mirrors the published DDS training and location information', function () { - $this->artisan('dds:seed-demo-events')->assertSuccessful(); + $this->pendingArtisan('dds:seed-demo-events')->assertSuccessful(); $season = Season::query() ->with(['events', 'seasonTicket']) @@ -177,7 +177,7 @@ CarbonImmutable::parse('2026-03-15 10:00:00', 'Europe/Amsterdam'), ); - $this->artisan('dds:seed-demo-events')->assertSuccessful(); + $this->pendingArtisan('dds:seed-demo-events')->assertSuccessful(); Event::query() ->whereIn('slug', DevelopmentEventSeeder::EVENT_SLUGS) @@ -200,7 +200,7 @@ }); test('reset removes only unreferenced demo records and preserves real content', function () { - $this->artisan('dds:seed-demo-events')->assertSuccessful(); + $this->pendingArtisan('dds:seed-demo-events')->assertSuccessful(); $demoLocation = Location::query() ->where('slug', DevelopmentEventSeeder::LOCATION_SLUG_PREFIX.'sportpaleis-alkmaar') @@ -237,7 +237,7 @@ 'slug' => DevelopmentEventSeeder::EVENT_SLUG_PREFIX.'community-record', ]); - $this->artisan('dds:seed-demo-events --reset') + $this->pendingArtisan('dds:seed-demo-events --reset') ->expectsOutput('10 demo-events verwijderd; overige content is behouden.') ->assertSuccessful(); @@ -257,7 +257,7 @@ }); test('reset works before the optional articles migration is applied', function () { - $this->artisan('dds:seed-demo-events')->assertSuccessful(); + $this->pendingArtisan('dds:seed-demo-events')->assertSuccessful(); $connection = DB::connection(); $connection->flushQueryLog(); @@ -269,7 +269,7 @@ ->andReturnFalse(); try { - $this->artisan('dds:seed-demo-events --reset') + $this->pendingArtisan('dds:seed-demo-events --reset') ->expectsOutput('10 demo-events verwijderd; overige content is behouden.') ->assertSuccessful(); @@ -292,7 +292,7 @@ app()->detectEnvironment(fn (): string => 'production'); try { - $this->artisan($command) + $this->pendingArtisan($command) ->expectsOutput('De DDS demo-events mogen alleen lokaal worden beheerd.') ->assertFailed(); } finally { @@ -315,6 +315,7 @@ expect(fn () => match ($method) { 'run' => $seeder->run(), 'reset' => $seeder->reset(), + default => throw new InvalidArgumentException("Unsupported seeder method [{$method}]."), }) ->toThrow(RuntimeException::class, 'De DDS demo-events mogen alleen lokaal worden beheerd.'); } finally { @@ -327,7 +328,7 @@ */ function demoEventsSnapshot(): array { - return Event::query() + $events = Event::query() ->whereIn('slug', DevelopmentEventSeeder::EVENT_SLUGS) ->with(['coverImage:id', 'coverImage.media', 'location:id,slug', 'season:id,name']) ->oldest('slug') @@ -351,20 +352,22 @@ function demoEventsSnapshot(): array 'cover' => $event->coverImage?->filename(), ]) ->all(); + + return array_values($events); } /** @return array> */ function demoRecordIds(): array { return [ - 'events' => Event::query()->whereIn('slug', DevelopmentEventSeeder::EVENT_SLUGS)->oldest('id')->pluck('id')->all(), - 'locations' => Location::query()->whereIn('slug', DevelopmentEventSeeder::LOCATION_SLUGS)->oldest('id')->pluck('id')->all(), - 'media' => MediaAsset::query()->whereHas('media', fn ($query) => $query - ->whereIn('custom_properties->development_fixture', DevelopmentEventSeeder::MEDIA_FIXTURE_KEYS))->oldest('id')->pluck('id')->all(), - 'seasons' => Season::query()->where('slug', DevelopmentEventSeeder::SEASON_SLUG)->oldest('id')->pluck('id')->all(), - 'seasonTickets' => SeasonTicket::query()->whereHas( + 'events' => array_values(Event::query()->whereIn('slug', DevelopmentEventSeeder::EVENT_SLUGS)->oldest('id')->pluck('id')->map(static fn (int $id): int => $id)->all()), + 'locations' => array_values(Location::query()->whereIn('slug', DevelopmentEventSeeder::LOCATION_SLUGS)->oldest('id')->pluck('id')->map(static fn (int $id): int => $id)->all()), + 'media' => array_values(MediaAsset::query()->whereHas('media', fn ($query) => $query + ->whereIn('custom_properties->development_fixture', DevelopmentEventSeeder::MEDIA_FIXTURE_KEYS))->oldest('id')->pluck('id')->map(static fn (int $id): int => $id)->all()), + 'seasons' => array_values(Season::query()->where('slug', DevelopmentEventSeeder::SEASON_SLUG)->oldest('id')->pluck('id')->map(static fn (int $id): int => $id)->all()), + 'seasonTickets' => array_values(SeasonTicket::query()->whereHas( 'season', fn ($query) => $query->where('slug', DevelopmentEventSeeder::SEASON_SLUG), - )->oldest('id')->pluck('id')->all(), + )->oldest('id')->pluck('id')->map(static fn (int $id): int => $id)->all()), ]; } diff --git a/tests/Feature/EventModelTest.php b/tests/Feature/EventModelTest.php index d288185..02e28f1 100644 --- a/tests/Feature/EventModelTest.php +++ b/tests/Feature/EventModelTest.php @@ -51,11 +51,11 @@ ->status->toBe(EventStatus::Published) ->type->toBe(EventType::Training) ->registration_status->toBe(EventRegistrationStatus::Open) - ->location->id->toBe($location->id) - ->season->id->toBe($season->id) - ->coverImage->id->toBe($coverImage->id) ->price_cents->toBe(1500) - ->capacity->toBe(16); + ->capacity->toBe(16) + ->and($event->location->id)->toBe($location->id) + ->and($event->season?->id)->toBe($season->id) + ->and($event->coverImage?->id)->toBe($coverImage->id); }); test('event enum values are enforced by the database', function (string $column) { diff --git a/tests/Feature/LocationModelTest.php b/tests/Feature/LocationModelTest.php index 9826b70..3121257 100644 --- a/tests/Feature/LocationModelTest.php +++ b/tests/Feature/LocationModelTest.php @@ -46,7 +46,7 @@ ->ceiling_height_metres->toBe('8.50') ->latitude->toBe('52.6320000') ->longitude->toBe('4.7450000') - ->coverImage->id->toBe($coverImage->id); + ->and($location->coverImage?->id)->toBe($coverImage->id); }); test('location environments are enforced by the database', function () { diff --git a/tests/Feature/MakeAdminUserCommandTest.php b/tests/Feature/MakeAdminUserCommandTest.php index 454a139..b575f9a 100644 --- a/tests/Feature/MakeAdminUserCommandTest.php +++ b/tests/Feature/MakeAdminUserCommandTest.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Hash; test('it creates a verified admin user', function () { - $this->artisan('dds:make-admin', [ + $this->pendingArtisan('dds:make-admin', [ 'email' => 'admin@example.com', '--name' => 'DDS Admin', '--password' => 'password', @@ -16,6 +16,7 @@ expect($user) ->name->toBe('DDS Admin') + ->email->toBeEmail() ->email_verified_at->not->toBeNull() ->and(Hash::check('password', $user->password))->toBeTrue() ->and($user->hasRole(Role::Admin->value))->toBeTrue() @@ -24,7 +25,7 @@ }); test('it generates a password when creating a new admin without a password option', function () { - $this->artisan('dds:make-admin', [ + $this->pendingArtisan('dds:make-admin', [ 'email' => 'generated@example.com', '--name' => 'Generated Admin', ]) @@ -47,7 +48,7 @@ 'password' => 'current-password', ]); - $this->artisan('dds:make-admin', [ + $this->pendingArtisan('dds:make-admin', [ 'email' => 'editor@example.com', ])->assertSuccessful(); @@ -61,7 +62,7 @@ }); test('it rejects invalid admin details without creating a user', function () { - $this->artisan('dds:make-admin', [ + $this->pendingArtisan('dds:make-admin', [ 'email' => 'not-an-email', '--name' => 'Invalid Admin', '--password' => 'password', diff --git a/tests/Feature/MediaAssetModelTest.php b/tests/Feature/MediaAssetModelTest.php index 9358644..9b16eba 100644 --- a/tests/Feature/MediaAssetModelTest.php +++ b/tests/Feature/MediaAssetModelTest.php @@ -24,13 +24,12 @@ $this->assertModelExists($mediaAsset); - expect($mediaAsset) - ->filename()->toBe('winter-race.jpg') - ->mimeType()->toBe('image/jpeg') - ->sizeBytes()->toBe(2048) - ->width()->toBe(1920) - ->height()->toBe(1080) - ->alt_text->toBe([ + expect($mediaAsset->filename())->toBe('winter-race.jpg') + ->and($mediaAsset->mimeType())->toBe('image/jpeg') + ->and($mediaAsset->sizeBytes())->toBe(2048) + ->and($mediaAsset->width())->toBe(1920) + ->and($mediaAsset->height())->toBe(1080) + ->and($mediaAsset->alt_text)->toBe([ 'en' => 'Pilots racing FPV drones indoors.', 'nl' => 'Piloten racen binnen met FPV-drones.', ]) diff --git a/tests/Feature/PartnerCatalogueTest.php b/tests/Feature/PartnerCatalogueTest.php index 221f104..c9cdf69 100644 --- a/tests/Feature/PartnerCatalogueTest.php +++ b/tests/Feature/PartnerCatalogueTest.php @@ -3,6 +3,10 @@ use App\Support\PartnerCatalogue; use App\Support\PartnerCatalogueEntry; +/** + * @param array $overrides + * @return array + */ function validPartnerCatalogueEntry(array $overrides = []): array { return array_replace([ @@ -21,8 +25,7 @@ function validPartnerCatalogueEntry(array $overrides = []): array $catalogue = PartnerCatalogue::fromConfig(); expect($catalogue->all()) - ->toHaveCount(2) - ->each->toBeInstanceOf(PartnerCatalogueEntry::class) + ->toHaveCount(2)->toContainOnlyInstancesOf(PartnerCatalogueEntry::class) ->and(collect($catalogue->all())->pluck('key')->all())->toBe([ 'droneshop-nl', 'sportpaleis-alkmaar', @@ -76,8 +79,8 @@ function validPartnerCatalogueEntry(array $overrides = []): array test('an empty partner catalogue is valid', function () { $catalogue = PartnerCatalogue::fromArray([]); - expect($catalogue->all())->toBe([]) - ->and($catalogue->forHomepage())->toBe([]) + expect($catalogue->all())->toBeEmpty() + ->and($catalogue->forHomepage())->toBeEmpty() ->and($catalogue->find('missing-partner'))->toBeNull(); }); diff --git a/tests/Feature/ProjectCatalogueTest.php b/tests/Feature/ProjectCatalogueTest.php index 1b27430..2a19b54 100644 --- a/tests/Feature/ProjectCatalogueTest.php +++ b/tests/Feature/ProjectCatalogueTest.php @@ -4,6 +4,10 @@ use App\Support\ProjectCatalogue; use App\Support\ProjectCatalogueEntry; +/** + * @param array $overrides + * @return array + */ function validProjectCatalogueEntry(array $overrides = []): array { return array_replace([ @@ -27,8 +31,7 @@ function validProjectCatalogueEntry(array $overrides = []): array $catalogue = ProjectCatalogue::fromConfig(); expect($catalogue->all()) - ->toHaveCount(9) - ->each->toBeInstanceOf(ProjectCatalogueEntry::class) + ->toHaveCount(9)->toContainOnlyInstancesOf(ProjectCatalogueEntry::class) ->and($catalogue->find('trackdraw')?->type)->toBe(ProjectType::Application) ->and($catalogue->find('panevo'))->toBeNull() ->and($catalogue->find('live-feed-flightcase')?->type)->toBe(ProjectType::HardwareBuild) diff --git a/tests/Feature/PublicEventPagesTest.php b/tests/Feature/PublicEventPagesTest.php index 8852d17..00fb595 100644 --- a/tests/Feature/PublicEventPagesTest.php +++ b/tests/Feature/PublicEventPagesTest.php @@ -9,6 +9,7 @@ use App\Models\MediaAsset; use App\Models\Season; use App\Models\SeasonTicket; +use Illuminate\Support\Collection; use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { @@ -184,7 +185,10 @@ ->where('season.events.0.registrationDeadlineAt', $openingEvent->registration_deadline_at?->toIso8601String()) ->where('season.events.1.id', $finalEvent->id) ->where('season.events.1.priceCents', 1750) - ->where('season.events', fn ($events): bool => collect($events)->doesntContain('id', $draftEvent->id)) + ->where( + 'season.events', + fn (Collection $events): bool => $events->doesntContain('id', $draftEvent->id), + ) ->where('seo.canonicalUrl', rtrim((string) config('app.url'), '/')."/seasons/{$season->slug}"), ); diff --git a/tests/Feature/PublicGettingStartedPageTest.php b/tests/Feature/PublicGettingStartedPageTest.php index 9b7cf07..4fb92fc 100644 --- a/tests/Feature/PublicGettingStartedPageTest.php +++ b/tests/Feature/PublicGettingStartedPageTest.php @@ -123,12 +123,11 @@ ); expect($slugs) - ->toHaveCount(count(array_unique($slugs))) + ->toHaveSameSize(array_unique($slugs)) ->and($sortOrders)->toBe(array_values(array_unique($sortOrders))); foreach ($guides as $guide) { - expect(is_file(resource_path("js/pages/public/getting-started/{$guide->slug}.tsx"))) - ->toBeTrue() + expect(resource_path("js/pages/public/getting-started/{$guide->slug}.tsx"))->toBeFile() ->and($guide->editorialOwner)->not->toBeEmpty() ->and($guide->reviewedAt)->toMatch('/^\d{4}-\d{2}-\d{2}$/'); } diff --git a/tests/Feature/PublicProjectPagesTest.php b/tests/Feature/PublicProjectPagesTest.php index 785b6f2..0cdc645 100644 --- a/tests/Feature/PublicProjectPagesTest.php +++ b/tests/Feature/PublicProjectPagesTest.php @@ -18,7 +18,13 @@ ->has('seo'), ); - $projects = collect($response->inertiaProps('projects')); + $projectProperties = $response->inertiaProps('projects'); + + if (! is_array($projectProperties)) { + throw new LogicException('The projects Inertia property must be an array.'); + } + + $projects = collect($projectProperties); $projectsBySlug = $projects->keyBy('slug'); $slugs = $projects->pluck('slug'); $trackdraw = $projectsBySlug['trackdraw']; diff --git a/tests/Feature/SeasonModelTest.php b/tests/Feature/SeasonModelTest.php index e101581..d09af9a 100644 --- a/tests/Feature/SeasonModelTest.php +++ b/tests/Feature/SeasonModelTest.php @@ -14,8 +14,8 @@ expect($season) ->name->toBe('Winter training series') ->slug->toBe('winter-training-series') - ->events->toHaveCount(3) - ->seasonTicket->toBeNull(); + ->and($season->events)->toHaveCount(3) + ->and($season->seasonTicket)->toBeNull(); }); test('season slugs are unique and remain stable when names change', function () { @@ -24,9 +24,8 @@ $firstSeason->update(['name' => 'Winter training series updated']); - expect($firstSeason->refresh()) - ->slug->toBe('winter-training-series') - ->getRouteKeyName()->toBe('slug') + expect($firstSeason->refresh()->slug)->toBe('winter-training-series') + ->and($firstSeason->getRouteKeyName())->toBe('slug') ->and($secondSeason->slug)->toBe('winter-training-series-2'); }); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index 870f8f3..81b97cd 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -33,9 +33,10 @@ $user->refresh(); - expect($user->name)->toBe('Test User'); - expect($user->email)->toBe('test@example.com'); - expect($user->email_verified_at)->toBeNull(); + expect($user->name)->toBe('Test User') + ->and($user->email)->toBeEmail() + ->toBe('test@example.com') + ->and($user->email_verified_at)->toBeNull(); }); test('email verification status is unchanged when the email address is unchanged', function () { diff --git a/tests/Pest.php b/tests/Pest.php index e1ff8f9..5e7f309 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -16,3 +16,7 @@ pest()->extend(TestCase::class) ->use(RefreshDatabase::class) ->in('Feature', 'Browser'); + +pest()->tia() + ->baselined() + ->filtered(); diff --git a/tests/TestCase.php b/tests/TestCase.php index a574f37..413a0f4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,10 +3,26 @@ namespace Tests; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Illuminate\Testing\PendingCommand; use Laravel\Fortify\Features; +use LogicException; abstract class TestCase extends BaseTestCase { + /** + * @param array $parameters + */ + protected function pendingArtisan(string $command, array $parameters = []): PendingCommand + { + $pendingCommand = $this->artisan($command, $parameters); + + if (! $pendingCommand instanceof PendingCommand) { + throw new LogicException('Console output mocking must be enabled for this test.'); + } + + return $pendingCommand; + } + protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void { if (! Features::enabled($feature)) { From 8583e6d3ce6dfdedafacef1a2f569d628a0cc94e Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 29 Jul 2026 16:52:43 +0200 Subject: [PATCH 2/2] Address Pest 5 review feedback --- composer.json | 2 +- tests/Feature/PublicEventPagesTest.php | 6 +++++- tests/Feature/PublicProjectPagesTest.php | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 2909ceb..283a7fb 100644 --- a/composer.json +++ b/composer.json @@ -90,7 +90,7 @@ "@test" ], "types:check": [ - "phpstan analyse" + "phpstan analyse --memory-limit=1G" ], "test": [ "@php artisan config:clear --ansi", diff --git a/tests/Feature/PublicEventPagesTest.php b/tests/Feature/PublicEventPagesTest.php index 00fb595..cede935 100644 --- a/tests/Feature/PublicEventPagesTest.php +++ b/tests/Feature/PublicEventPagesTest.php @@ -187,7 +187,11 @@ ->where('season.events.1.priceCents', 1750) ->where( 'season.events', - fn (Collection $events): bool => $events->doesntContain('id', $draftEvent->id), + fn (mixed $events): bool => match (true) { + $events instanceof Collection => $events->doesntContain('id', $draftEvent->id), + is_array($events) => (new Collection($events))->doesntContain('id', $draftEvent->id), + default => false, + }, ) ->where('seo.canonicalUrl', rtrim((string) config('app.url'), '/')."/seasons/{$season->slug}"), ); diff --git a/tests/Feature/PublicProjectPagesTest.php b/tests/Feature/PublicProjectPagesTest.php index 0cdc645..1eebd56 100644 --- a/tests/Feature/PublicProjectPagesTest.php +++ b/tests/Feature/PublicProjectPagesTest.php @@ -20,8 +20,10 @@ $projectProperties = $response->inertiaProps('projects'); + expect(get_debug_type($projectProperties))->toBe('array'); + if (! is_array($projectProperties)) { - throw new LogicException('The projects Inertia property must be an array.'); + return; } $projects = collect($projectProperties);