diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..92b5bf56
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+# Code of Conduct
+
+The Laravel Code of Conduct can be found in the [Laravel documentation](https://laravel.com/docs/contributions#code-of-conduct).
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 0944fe4d..00000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-github: [reinink]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..03786937
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,9 @@
+
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 00000000..800b8aff
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,92 @@
+# Security Policy
+
+**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
+
+## Supported Versions
+
+Only the latest major version receives security fixes.
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability within Laravel, please send an email to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed.
+
+### Public PGP Key
+
+```
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: OpenPGP v2.0.8
+Comment: Report Security Vulnerabilities to taylor@laravel.com
+
+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
+WBGOG/qJGDlNiqBYYt2xNqzHCJoC
+=zXOv
+-----END PGP PUBLIC KEY BLOCK-----
+```
diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md
new file mode 100644
index 00000000..f0877fc2
--- /dev/null
+++ b/.github/SUPPORT.md
@@ -0,0 +1,3 @@
+# Support Questions
+
+The Laravel support guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions#support-questions).
diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml
new file mode 100644
index 00000000..24228e47
--- /dev/null
+++ b/.github/workflows/coding-standards.yml
@@ -0,0 +1,12 @@
+name: fix code styling
+
+on: [push]
+
+permissions:
+ contents: write
+
+jobs:
+ lint:
+ uses: laravel/.github/.github/workflows/coding-standards.yml@main
+ with:
+ php: "8.3"
diff --git a/.github/workflows/facade.yml b/.github/workflows/facade.yml
index 115daf3d..b2917730 100644
--- a/.github/workflows/facade.yml
+++ b/.github/workflows/facade.yml
@@ -3,7 +3,7 @@ name: facades
on:
push:
branches:
- - 'master'
+ - "master"
jobs:
update:
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7060e6e7..3591aea7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,53 +8,25 @@ on:
jobs:
tests:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
strategy:
fail-fast: true
matrix:
- php: [7.3, 7.4, "8.0", 8.1, 8.2, 8.3]
- laravel: [8, 9, 10, 11]
+ php: [8.1, 8.2, 8.3, 8.4]
+ laravel: [10, 11, 12]
stability: ["prefer-lowest", "prefer-stable"]
exclude:
- - php: 7.3
- laravel: 9
- - php: 7.3
+ - php: 8.4
laravel: 10
- - php: 7.3
- laravel: 11
- - php: 7.4
- laravel: 9
- - php: 7.4
- laravel: 10
- - php: 7.4
- laravel: 11
- - php: '8.0'
- laravel: 10
- - php: '8.0'
- laravel: 11
- - php: 8.1
- laravel: 6
- - php: 8.1
- laravel: 7
- php: 8.1
laravel: 11
- - php: 8.2
- laravel: 6
- - php: 8.2
- laravel: 7
- - php: 8.2
- laravel: 8
- - php: 8.3
- laravel: 6
- - php: 8.3
- laravel: 7
- - php: 8.3
- laravel: 8
+ - php: 8.1
+ laravel: 12
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} (w/ ${{ matrix.stability }})
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -66,42 +38,41 @@ jobs:
coverage: none
- name: Set Minimum PHP 8.1 Versions
- uses: nick-invision/retry@v1
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: |
- composer require phpunit/phpunit:^9.5.8 --dev --${{ matrix.stability }} --no-update --no-interaction
composer require vlucas/phpdotenv:^5.3.1 --${{ matrix.stability }} --no-update --no-interaction
if: matrix.php >= 8.1 && matrix.stability == 'prefer-lowest'
- name: Set Minimum PHP 8.2 Versions
- uses: nick-invision/retry@v1
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: |
composer require nesbot/carbon:^2.62.1 --dev --${{ matrix.stability }} --no-update --no-interaction
- if: matrix.php >= 8.2 && matrix.stability == 'prefer-lowest'
+ if: matrix.php >= 8.2 && matrix.stability == 'prefer-lowest' && matrix.laravel < 12
- name: Set Minimum PHP 8.2 Versions and Laravel > 11
- uses: nick-invision/retry@v1
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: |
- composer require phpunit/phpunit:^10.4 --dev --${{ matrix.stability }} --no-update --no-interaction
+ composer require "orchestra/testbench:^9.2|^10.0" --dev --${{ matrix.stability }} --no-update --no-interaction
if: matrix.php >= 8.2 && matrix.stability == 'prefer-lowest' && matrix.laravel >= 11
- name: Set Laravel version
- uses: nick-invision/retry@v1
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: composer require "laravel/framework=^${{ matrix.laravel }}" --no-interaction --no-update
- name: Install dependencies
- uses: nick-invision/retry@v1
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
deleted file mode 100644
index cc0b20ff..00000000
--- a/.php-cs-fixer.dist.php
+++ /dev/null
@@ -1,28 +0,0 @@
-setUsingCache(false)
- ->setRiskyAllowed(true)
- ->setRules([
- '@PHP70Migration' => true,
- '@PHP71Migration' => true,
- '@PSR2' => true,
- '@Symfony' => true,
- 'array_syntax' => ['syntax' => 'short'],
- 'increment_style' => ['style' => 'post'],
- 'multiline_whitespace_before_semicolons' => true,
- 'array_indentation' => true,
- 'not_operator_with_successor_space' => true,
- 'ordered_imports' => ['sort_algorithm' => 'length'],
- 'php_unit_method_casing' => ['case' => 'snake_case'],
- 'semicolon_after_instruction' => false,
- 'single_line_throw' => false,
- 'yoda_style' => false,
- 'strict_comparison' => true,
- 'yoda_style' => false,
- 'single_line_throw' => false,
- 'php_unit_method_casing' => ['case' => 'snake_case'],
- 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true],
- ]);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab6fa08b..41da63bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,62 @@
# Release Notes
-## [Unreleased](https://github.com/inertiajs/inertia-laravel/compare/v1.2.0...1.x)
+## [Unreleased](https://github.com/inertiajs/inertia-laravel/compare/v2.0.2...2.x)
-- Add "always" props using new `Inertia::always()` wrapper ([#627](https://github.com/inertiajs/inertia-laravel/pull/627))
+- Nothing!
+
+## [v2.0.2](https://github.com/inertiajs/inertia-laravel/compare/v2.0.1...v2.0.2) - 2025-04-10
+
+### What's Changed
+
+* [2.x] Supports Laravel 12 by [@crynobone](https://github.com/crynobone) in https://github.com/inertiajs/inertia-laravel/pull/709
+* Add Inertia::deepMerge Method for Handling Complex Data Merges in Responses by [@HichemTab-tech](https://github.com/HichemTab-tech) in https://github.com/inertiajs/inertia-laravel/pull/679
+* Improve PHPDoc annotations for ResponseFactory class by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/inertiajs/inertia-laravel/pull/723
+* fix props that extends Responsable after closures / lazy props by [@d8vjork](https://github.com/d8vjork) in https://github.com/inertiajs/inertia-laravel/pull/722
+* [2.x] Allow environment config for `ssr.enabled`, `ssr.url`, and `history.encrypt` by [@bram-pkg](https://github.com/bram-pkg) in https://github.com/inertiajs/inertia-laravel/pull/714
+* Replace `array_merge` with spread operator in `middleware.stub` by [@osbre](https://github.com/osbre) in https://github.com/inertiajs/inertia-laravel/pull/710
+* [2.x] Resolve Closure before checking if a prop implements the Arrayable contract by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/inertiajs/inertia-laravel/pull/706
+* Handle SSR URLs with trailing slashes by [@simon-tma](https://github.com/simon-tma) in https://github.com/inertiajs/inertia-laravel/pull/705
+* [2.x] Call `toArray()` on `Arrayable` props resolved from the Container by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia-laravel/pull/696
+* [2.x] Replace md5 with xxhash by [@RobertBoes](https://github.com/RobertBoes) in https://github.com/inertiajs/inertia-laravel/pull/653
+
+### New Contributors
+
+* [@HichemTab-tech](https://github.com/HichemTab-tech) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/679
+* [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/723
+* [@d8vjork](https://github.com/d8vjork) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/722
+* [@bram-pkg](https://github.com/bram-pkg) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/714
+* [@osbre](https://github.com/osbre) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/710
+* [@simon-tma](https://github.com/simon-tma) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/705
+* [@pascalbaljet](https://github.com/pascalbaljet) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/696
+
+**Full Changelog**: https://github.com/inertiajs/inertia-laravel/compare/v2.0.1...v2.0.2
+
+## [v2.0.1](https://github.com/inertiajs/inertia-laravel/compare/v2.0.0...v2.0.1) - 2025-02-18
+
+- Allow Laravel 12.x.
+
+**Full Changelog**: https://github.com/inertiajs/inertia-laravel/compare/v2.0.0...v2.0.1
+
+## [v2.0.0](https://github.com/inertiajs/inertia-laravel/compare/v1.2.0...2.0.0)
+
+- Add support for Inertia.js v2.0.0
+- Add `Inertia::defer()` to support deferred props
+- Add `Inertia::merge()` to support merging props on client
+- Add `Inertia::always()` for props that should always be included ([#627](https://github.com/inertiajs/inertia-laravel/pull/627))
+- Add `Inertia::clearHistory()` and `Inertia::encryptHistory()` methods, encryption config, and encryption middleware
+- Deprecated `Inertia::lazy()` in favor of `Inertia::optional()`
+- Drop support for Laravel 8 and 9 ([#629](https://github.com/inertiajs/inertia-laravel/pull/629))
## [v1.2.0](https://github.com/inertiajs/inertia-laravel/compare/v1.1.0...v1.2.0) - 2024-05-17
-* [1.x] Make commands lazy by [@timacdonald](https://github.com/timacdonald) in https://github.com/inertiajs/inertia-laravel/pull/601
-* [1.x] Persistent properties by [@lepikhinb](https://github.com/lepikhinb) in https://github.com/inertiajs/inertia-laravel/pull/621
-* [1.x] Exclude properties from partial responses by [@lepikhinb](https://github.com/lepikhinb) in https://github.com/inertiajs/inertia-laravel/pull/622
+- Make commands lazy ([#601](https://github.com/inertiajs/inertia-laravel/pull/601))
+- Add persistent properties ([#621](https://github.com/inertiajs/inertia-laravel/pull/621))
+- Exclude `except` props from partial reloads ([#622](https://github.com/inertiajs/inertia-laravel/pull/622))
## [v1.1.0](https://github.com/inertiajs/inertia-laravel/compare/v1.0.0...v1.1.0) - 2024-05-16
-* Support dot notation in partial requests by [@lepikhinb](https://github.com/lepikhinb) in https://github.com/inertiajs/inertia-laravel/pull/620
-* [1.x] Add `$request->inertia()` IDE helper by [@ycs77](https://github.com/ycs77) in https://github.com/inertiajs/inertia-laravel/pull/625
+- Support dot notation in partial requests ([#620](https://github.com/inertiajs/inertia-laravel/pull/620))
+- Add `$request->inertia()` IDE helper ([#625](https://github.com/inertiajs/inertia-laravel/pull/625))
## [v1.0.0](https://github.com/inertiajs/inertia-laravel/compare/v0.6.11...v1.0.0) - 2024-03-08
diff --git a/README.md b/README.md
index 52d49ba8..96903f31 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,8 @@
-
-
+
+
@@ -13,3 +13,19 @@
This is some example SSR content
\n"; diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php new file mode 100644 index 00000000..85e94c53 --- /dev/null +++ b/tests/HistoryTest.php @@ -0,0 +1,165 @@ +get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => false, + 'clearHistory' => false, + ]); + } + + public function test_the_history_can_be_encrypted(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::encryptHistory(); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_via_middleware(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class, EncryptHistoryMiddleware::class])->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_via_middleware_alias(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class, 'inertia.encrypt'])->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_globally(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Config::set('inertia.history.encrypt', true); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_globally_and_overridden(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Config::set('inertia.history.encrypt', true); + + Inertia::encryptHistory(false); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => false, + ]); + } + + public function test_the_history_can_be_cleared(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::clearHistory(); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'clearHistory' => true, + ]); + } + + public function test_the_history_can_be_cleared_when_redirecting(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::clearHistory(); + + return redirect('/users'); + }); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/users', function () { + return Inertia::render('User/Edit'); + }); + + $this->followingRedirects(); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertContent(''); + } +} diff --git a/tests/LazyPropTest.php b/tests/LazyPropTest.php index d8ae0e50..f1127328 100644 --- a/tests/LazyPropTest.php +++ b/tests/LazyPropTest.php @@ -2,8 +2,8 @@ namespace Inertia\Tests; -use Inertia\LazyProp; use Illuminate\Http\Request; +use Inertia\LazyProp; class LazyPropTest extends TestCase { diff --git a/tests/MergePropTest.php b/tests/MergePropTest.php new file mode 100644 index 00000000..dfd21213 --- /dev/null +++ b/tests/MergePropTest.php @@ -0,0 +1,34 @@ +assertSame('A merge prop value', $mergeProp()); + } + + public function test_can_invoke_with_a_non_callback(): void + { + $mergeProp = new MergeProp(['key' => 'value']); + + $this->assertSame(['key' => 'value'], $mergeProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $mergeProp = new MergeProp(function (Request $request) { + return $request; + }); + + $this->assertInstanceOf(Request::class, $mergeProp()); + } +} diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 4fb9f5d4..75102a66 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -2,20 +2,29 @@ namespace Inertia\Tests; -use LogicException; -use Inertia\Inertia; -use Inertia\Middleware; +use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Request; -use Illuminate\Support\MessageBag; -use Illuminate\Support\ViewErrorBag; +use Illuminate\Session\Middleware\StartSession; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Session; -use Inertia\Tests\Stubs\ExampleMiddleware; -use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\MessageBag; +use Illuminate\Support\ViewErrorBag; use Inertia\AlwaysProp; +use Inertia\Inertia; +use Inertia\Middleware; +use Inertia\Tests\Stubs\CustomUrlResolverMiddleware; +use Inertia\Tests\Stubs\ExampleMiddleware; +use LogicException; +use PHPUnit\Framework\Attributes\After; class MiddlewareTest extends TestCase { + #[After] + public function cleanupPublicFolder(): void + { + (new Filesystem)->cleanDirectory(public_path()); + } + public function test_no_response_value_by_default_means_automatically_redirecting_back_for_inertia_requests(): void { $fooCalled = false; @@ -122,6 +131,21 @@ public function test_it_will_instruct_inertia_to_reload_on_a_version_mismatch(): self::assertEmpty($response->getContent()); } + public function test_the_url_can_be_resolved_with_a_custom_resolver() + { + $this->prepareMockEndpoint(middleware: new CustomUrlResolverMiddleware); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'url' => '/my-custom-url', + ]); + } + public function test_validation_errors_are_registered_as_of_default(): void { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { @@ -145,7 +169,7 @@ public function test_validation_errors_can_be_empty(): void public function test_validation_errors_are_returned_in_the_correct_format(): void { - Session::put('errors', (new ViewErrorBag())->put('default', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('default', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -163,7 +187,7 @@ public function test_validation_errors_are_returned_in_the_correct_format(): voi public function test_validation_errors_with_named_error_bags_are_scoped(): void { - Session::put('errors', (new ViewErrorBag())->put('example', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('example', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -181,7 +205,7 @@ public function test_validation_errors_with_named_error_bags_are_scoped(): void public function test_default_validation_errors_can_be_overwritten(): void { - Session::put('errors', (new ViewErrorBag())->put('example', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('example', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -198,7 +222,7 @@ public function test_default_validation_errors_can_be_overwritten(): void public function test_validation_errors_are_scoped_to_error_bag_header(): void { - Session::put('errors', (new ViewErrorBag())->put('default', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('default', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -216,7 +240,8 @@ public function test_validation_errors_are_scoped_to_error_bag_header(): void public function test_middleware_can_change_the_root_view_via_a_property(): void { - $this->prepareMockEndpoint(null, [], new class() extends Middleware { + $this->prepareMockEndpoint(null, [], new class extends Middleware + { protected $rootView = 'welcome'; }); @@ -227,7 +252,8 @@ public function test_middleware_can_change_the_root_view_via_a_property(): void public function test_middleware_can_change_the_root_view_by_overriding_the_rootview_method(): void { - $this->prepareMockEndpoint(null, [], new class() extends Middleware { + $this->prepareMockEndpoint(null, [], new class extends Middleware + { public function rootView(Request $request): string { return 'welcome'; @@ -239,6 +265,49 @@ public function rootView(Request $request): string $response->assertViewIs('welcome'); } + public function test_determine_the_version_by_a_hash_of_the_asset_url(): void + { + config(['app.asset_url' => $url = 'https://example.com/assets']); + + $this->prepareMockEndpoint(middleware: new Middleware); + + $response = $this->get('/'); + $response->assertOk(); + $response->assertViewHas('page.version', hash('xxh128', $url)); + } + + public function test_determine_the_version_by_a_hash_of_the_vite_manifest(): void + { + $filesystem = new Filesystem; + $filesystem->ensureDirectoryExists(public_path('build')); + $filesystem->put( + public_path('build/manifest.json'), + $contents = json_encode(['vite' => true]) + ); + + $this->prepareMockEndpoint(middleware: new Middleware); + + $response = $this->get('/'); + $response->assertOk(); + $response->assertViewHas('page.version', hash('xxh128', $contents)); + } + + public function test_determine_the_version_by_a_hash_of_the_mix_manifest(): void + { + $filesystem = new Filesystem; + $filesystem->ensureDirectoryExists(public_path()); + $filesystem->put( + public_path('mix-manifest.json'), + $contents = json_encode(['mix' => true]) + ); + + $this->prepareMockEndpoint(middleware: new Middleware); + + $response = $this->get('/'); + $response->assertOk(); + $response->assertViewHas('page.version', hash('xxh128', $contents)); + } + private function prepareMockEndpoint($version = null, $shared = [], $middleware = null): \Illuminate\Routing\Route { if (is_null($middleware)) { diff --git a/tests/OptionalPropTest.php b/tests/OptionalPropTest.php new file mode 100644 index 00000000..932c803b --- /dev/null +++ b/tests/OptionalPropTest.php @@ -0,0 +1,27 @@ +assertSame('A lazy value', $optionalProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $optionalProp = new OptionalProp(function (Request $request) { + return $request; + }); + + $this->assertInstanceOf(Request::class, $optionalProp()); + } +} diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index b86754b9..c2af15ad 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -2,26 +2,29 @@ namespace Inertia\Tests; -use Inertia\Inertia; -use Inertia\LazyProp; -use Inertia\ResponseFactory; -use Illuminate\Http\Response; -use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Request; -use Inertia\Tests\Stubs\ExampleMiddleware; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request as HttpRequest; +use Illuminate\Http\Response; use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\NullSessionHandler; use Illuminate\Session\Store; +use Illuminate\Support\Facades\Request; +use Illuminate\Support\Facades\Route; use Inertia\AlwaysProp; +use Inertia\DeferProp; +use Inertia\Inertia; +use Inertia\LazyProp; +use Inertia\MergeProp; +use Inertia\OptionalProp; +use Inertia\ResponseFactory; +use Inertia\Tests\Stubs\ExampleMiddleware; class ResponseFactoryTest extends TestCase { public function test_can_macro(): void { - $factory = new ResponseFactory(); + $factory = new ResponseFactory; $factory->macro('foo', function () { return 'bar'; }); @@ -35,7 +38,7 @@ public function test_location_response_for_inertia_requests(): void return true; }); - $response = (new ResponseFactory())->location('https://inertiajs.com'); + $response = (new ResponseFactory)->location('https://inertiajs.com'); $this->assertInstanceOf(Response::class, $response); $this->assertEquals(Response::HTTP_CONFLICT, $response->getStatusCode()); @@ -48,7 +51,7 @@ public function test_location_response_for_non_inertia_requests(): void return false; }); - $response = (new ResponseFactory())->location('https://inertiajs.com'); + $response = (new ResponseFactory)->location('https://inertiajs.com'); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -62,7 +65,7 @@ public function test_location_response_for_inertia_requests_using_redirect_respo }); $redirect = new RedirectResponse('https://inertiajs.com'); - $response = (new ResponseFactory())->location($redirect); + $response = (new ResponseFactory)->location($redirect); $this->assertInstanceOf(Response::class, $response); $this->assertEquals(409, $response->getStatusCode()); @@ -72,7 +75,7 @@ public function test_location_response_for_inertia_requests_using_redirect_respo public function test_location_response_for_non_inertia_requests_using_redirect_response(): void { $redirect = new RedirectResponse('https://inertiajs.com'); - $response = (new ResponseFactory())->location($redirect); + $response = (new ResponseFactory)->location($redirect); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -81,7 +84,7 @@ public function test_location_response_for_non_inertia_requests_using_redirect_r public function test_location_redirects_are_not_modified(): void { - $response = (new ResponseFactory())->location('/foo'); + $response = (new ResponseFactory)->location('/foo'); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -93,7 +96,7 @@ public function test_location_response_for_non_inertia_requests_using_redirect_r $redirect = new RedirectResponse('https://inertiajs.com'); $redirect->setSession($session = new Store('test', new NullSessionHandler)); $redirect->setRequest($request = new HttpRequest); - $response = (new ResponseFactory())->location($redirect); + $response = (new ResponseFactory)->location($redirect); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -109,7 +112,7 @@ public function test_the_version_can_be_a_closure(): void $this->assertSame('', Inertia::getVersion()); Inertia::version(function () { - return md5('Inertia'); + return hash('xxh128', 'Inertia'); }); return Inertia::render('User/Edit'); @@ -117,13 +120,37 @@ public function test_the_version_can_be_a_closure(): void $response = $this->withoutExceptionHandling()->get('/', [ 'X-Inertia' => 'true', - 'X-Inertia-Version' => 'b19a24ee5c287f42ee1d465dab77ab37', + 'X-Inertia-Version' => 'f445bd0a2c393a5af14fc677f59980a9', ]); $response->assertSuccessful(); $response->assertJson(['component' => 'User/Edit']); } + public function test_the_url_can_be_resolved_with_a_custom_resolver() + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::resolveUrlUsing(function ($request, ResponseFactory $otherDependency) { + $this->assertInstanceOf(HttpRequest::class, $request); + $this->assertInstanceOf(ResponseFactory::class, $otherDependency); + + return '/my-custom-url'; + }); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'url' => '/my-custom-url', + ]); + } + public function test_shared_data_can_be_shared_from_anywhere(): void { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { @@ -143,6 +170,87 @@ public function test_shared_data_can_be_shared_from_anywhere(): void ]); } + public function test_dot_props_are_merged_from_shared(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::share('auth.user', [ + 'name' => 'Jonathan', + ]); + + return Inertia::render('User/Edit', [ + 'auth.user.can.create_group' => false, + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'auth' => [ + 'user' => [ + 'name' => 'Jonathan', + 'can' => [ + 'create_group' => false, + ], + ], + ], + ], + ]); + } + + public function test_shared_data_can_resolve_closure_arguments(): void + { + Inertia::share('query', fn (HttpRequest $request) => $request->query()); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/?foo=bar', ['X-Inertia' => 'true']); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'query' => [ + 'foo' => 'bar', + ], + ], + ]); + } + + public function test_dot_props_with_callbacks_are_merged_from_shared(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::share('auth.user', fn () => [ + 'name' => 'Jonathan', + ]); + + return Inertia::render('User/Edit', [ + 'auth.user.can.create_group' => false, + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'auth' => [ + 'user' => [ + 'name' => 'Jonathan', + 'can' => [ + 'create_group' => false, + ], + ], + ], + ], + ]); + } + public function test_can_flush_shared_data(): void { Inertia::share('foo', 'bar'); @@ -153,7 +261,7 @@ public function test_can_flush_shared_data(): void public function test_can_create_lazy_prop(): void { - $factory = new ResponseFactory(); + $factory = new ResponseFactory; $lazyProp = $factory->lazy(function () { return 'A lazy value'; }); @@ -161,9 +269,81 @@ public function test_can_create_lazy_prop(): void $this->assertInstanceOf(LazyProp::class, $lazyProp); } + public function test_can_create_deferred_prop(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred value'; + }); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + $this->assertSame($deferredProp->group(), 'default'); + } + + public function test_can_create_deferred_prop_with_custom_group(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred value'; + }, 'foo'); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + $this->assertSame($deferredProp->group(), 'foo'); + } + + public function test_can_create_merged_prop(): void + { + $factory = new ResponseFactory; + $mergedProp = $factory->merge(function () { + return 'A merged value'; + }); + + $this->assertInstanceOf(MergeProp::class, $mergedProp); + } + + public function test_can_create_deep_merged_prop(): void + { + $factory = new ResponseFactory; + $mergedProp = $factory->deepMerge(function () { + return 'A merged value'; + }); + + $this->assertInstanceOf(MergeProp::class, $mergedProp); + } + + public function test_can_create_deferred_and_merged_prop(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred + merged value'; + })->merge(); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + } + + public function test_can_create_deferred_and_deep_merged_prop(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred + merged value'; + })->deepMerge(); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + } + + public function test_can_create_optional_prop(): void + { + $factory = new ResponseFactory; + $optionalProp = $factory->optional(function () { + return 'An optional value'; + }); + + $this->assertInstanceOf(OptionalProp::class, $optionalProp); + } + public function test_can_create_always_prop(): void { - $factory = new ResponseFactory(); + $factory = new ResponseFactory; $alwaysProp = $factory->always(function () { return 'An always value'; }); @@ -176,7 +356,8 @@ public function test_will_accept_arrayabe_props() Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { Inertia::share('foo', 'bar'); - return Inertia::render('User/Edit', new class() implements Arrayable { + return Inertia::render('User/Edit', new class implements Arrayable + { public function toArray() { return [ diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index d36cdaae..a262db3d 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -2,20 +2,23 @@ namespace Inertia\Tests; -use Mockery; -use Inertia\LazyProp; -use Inertia\Response; -use Illuminate\View\View; -use Illuminate\Http\Request; -use Illuminate\Support\Fluent; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\JsonResponse; -use Illuminate\Support\Collection; -use Inertia\Tests\Stubs\FakeResource; -use Illuminate\Http\Response as BaseResponse; -use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; +use Illuminate\Http\Response as BaseResponse; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; +use Illuminate\Support\Fluent; +use Illuminate\View\View; use Inertia\AlwaysProp; +use Inertia\DeferProp; +use Inertia\LazyProp; +use Inertia\MergeProp; +use Inertia\Response; +use Inertia\Tests\Stubs\FakeResource; +use Mockery; class ResponseTest extends TestCase { @@ -46,7 +49,337 @@ public function test_server_response(): void $this->assertSame('Jonathan', $page['props']['user']['name']); $this->assertSame('/user/123', $page['url']); $this->assertSame('123', $page['version']); - $this->assertSame('', $view->render()); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_deferred_prop(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new DeferProp(function () { + return 'bar'; + }, 'default'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo'], + ], $page['deferredProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_deferred_prop_and_multiple_groups(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new DeferProp(function () { + return 'foo value'; + }, 'default'), + 'bar' => new DeferProp(function () { + return 'bar value'; + }, 'default'), + 'baz' => new DeferProp(function () { + return 'baz value'; + }, 'custom'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo', 'bar'], + 'custom' => ['baz'], + ], $page['deferredProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new MergeProp('foo value'), + 'bar' => new MergeProp('bar value'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['mergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_deep_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new MergeProp('foo value'))->deepMerge(), + 'bar' => (new MergeProp('bar value'))->deepMerge(), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['deepMergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_merge_strategies(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new MergeProp('foo value', ['foo-key']))->deepMerge(), + 'bar' => (new MergeProp('bar value', ['bar-key']))->deepMerge(), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['deepMergeProps']); + $this->assertSame([ + 'foo.foo-key', + 'bar.bar-key', + ], $page['mergeStrategies']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_defer_and_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new DeferProp(function () { + return 'foo value'; + }, 'default'))->merge(), + 'bar' => new MergeProp('bar value'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo'], + ], $page['deferredProps']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['mergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_server_response_with_defer_and_deep_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new DeferProp(function () { + return 'foo value'; + }, 'default'))->deepMerge(), + 'bar' => (new MergeProp('bar value'))->deepMerge(), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo'], + ], $page['deferredProps']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['deepMergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('', $view->render()); + } + + public function test_exclude_merge_props_from_partial_only_response(): void + { + $request = Request::create('/user/123', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Component' => 'User/Edit']); + $request->headers->add(['X-Inertia-Partial-Data' => 'user']); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new MergeProp('foo value'), + 'bar' => new MergeProp('bar value'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $page = $response->getData(); + + $props = get_object_vars($page->props); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $this->assertSame('Jonathan', $props['user']->name); + $this->assertArrayNotHasKey('foo', $props); + $this->assertArrayNotHasKey('bar', $props); + $this->assertFalse(isset($page->mergeProps)); + } + + public function test_exclude_merge_props_from_partial_except_response(): void + { + $request = Request::create('/user/123', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Component' => 'User/Edit']); + $request->headers->add(['X-Inertia-Partial-Except' => 'foo']); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new MergeProp('foo value'), + 'bar' => new MergeProp('bar value'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $page = $response->getData(); + + $props = get_object_vars($page->props); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $this->assertSame('Jonathan', $props['user']->name); + $this->assertArrayNotHasKey('foo', $props); + $this->assertArrayHasKey('bar', $props); + $this->assertSame(['bar'], $page->mergeProps); } public function test_xhr_response(): void @@ -84,6 +417,54 @@ public function test_resource_response(): void $this->assertSame('123', $page->version); } + public function test_lazy_callable_resource_response(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', [ + 'users' => fn () => [['name' => 'Jonathan']], + 'organizations' => fn () => [['name' => 'Inertia']], + ], 'app', '123'); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame('User/Index', $page->component); + $this->assertSame('/users', $page->url); + $this->assertSame('123', $page->version); + tap($page->props->users, function ($users) { + $this->assertSame(json_encode([['name' => 'Jonathan']]), json_encode($users)); + }); + tap($page->props->organizations, function ($organizations) { + $this->assertSame(json_encode([['name' => 'Inertia']]), json_encode($organizations)); + }); + } + + public function test_lazy_callable_resource_partial_response(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Data' => 'users']); + $request->headers->add(['X-Inertia-Partial-Component' => 'User/Index']); + + $response = new Response('User/Index', [ + 'users' => fn () => [['name' => 'Jonathan']], + 'organizations' => fn () => [['name' => 'Inertia']], + ], 'app', '123'); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame('User/Index', $page->component); + $this->assertSame('/users', $page->url); + $this->assertSame('123', $page->version); + $this->assertFalse(property_exists($page->props, 'organizations')); + tap($page->props->users, function ($users) { + $this->assertSame(json_encode([['name' => 'Jonathan']]), json_encode($users)); + }); + } + public function test_lazy_resource_response(): void { $request = Request::create('/users', 'GET', ['page' => 1]); @@ -98,8 +479,7 @@ public function test_lazy_resource_response(): void $callable = static function () use ($users) { $page = new LengthAwarePaginator($users->take(2), $users->count(), 2); - return new class($page, JsonResource::class) extends ResourceCollection { - }; + return new class($page, JsonResource::class) extends ResourceCollection {}; }; $response = new Response('User/Index', ['users' => $callable], 'app', '123'); @@ -381,6 +761,31 @@ public function test_lazy_props_are_included_in_partial_reload(): void $this->assertSame('A lazy value', $page->props->lazy); } + public function test_defer_arrayable_props_are_resolved_in_partial_reload(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Component' => 'Users']); + $request->headers->add(['X-Inertia-Partial-Data' => 'defer']); + + $deferProp = new DeferProp(function () { + return new class implements Arrayable + { + public function toArray() + { + return ['foo' => 'bar']; + } + }; + }); + + $response = new Response('Users', ['users' => [], 'defer' => $deferProp], 'app', '123'); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertFalse(property_exists($page->props, 'users')); + $this->assertEquals((object) ['foo' => 'bar'], $page->props->defer); + } + public function test_always_props_are_included_on_partial_reload(): void { $request = Request::create('/user/123', 'GET'); @@ -400,9 +805,9 @@ public function test_always_props_are_included_on_partial_reload(): void ], 'errors' => new AlwaysProp(function () { return [ - 'name' => 'The email field is required.' + 'name' => 'The email field is required.', ]; - }) + }), ]; $response = new Response('User/Edit', $props, 'app', '123'); @@ -523,4 +928,52 @@ public function test_the_page_url_doesnt_double_up(): void $this->assertSame('/subpath/product/123', $page->url); } + + public function test_trailing_slashes_in_a_url_are_preserved(): void + { + $request = Request::create('/users/', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users/', $page->url); + } + + public function test_trailing_slashes_in_a_url_with_query_parameters_are_preserved(): void + { + $request = Request::create('/users/?page=1&sort=name', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users/?page=1&sort=name', $page->url); + } + + public function test_a_url_without_trailing_slash_is_resolved_correctly(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users', $page->url); + } + + public function test_a_url_without_trailing_slash_and_query_parameters_is_resolved_correctly(): void + { + $request = Request::create('/users?page=1&sort=name', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users?page=1&sort=name', $page->url); + } } diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php index 28fe55fe..efb270b4 100644 --- a/tests/ServiceProviderTest.php +++ b/tests/ServiceProviderTest.php @@ -30,10 +30,13 @@ public function test_route_macro_is_registered(): void $routes = Route::getRoutes(); $this->assertNotEmpty($routes->getRoutes()); - $this->assertEquals($route, $routes->getRoutes()[0]); - $this->assertEquals(['GET', 'HEAD'], $route->methods); - $this->assertEquals('/', $route->uri); - $this->assertEquals(['uses' => '\Inertia\Controller@__invoke', 'controller' => '\Inertia\Controller'], $route->action); - $this->assertEquals(['component' => 'User/Edit', 'props' => ['user' => ['name' => 'Jonathan']]], $route->defaults); + + $inertiaRoute = collect($routes->getRoutes())->first(fn ($route) => $route->uri === '/'); + + $this->assertEquals($route, $inertiaRoute); + $this->assertEquals(['GET', 'HEAD'], $inertiaRoute->methods); + $this->assertEquals('/', $inertiaRoute->uri); + $this->assertEquals(['uses' => '\Inertia\Controller@__invoke', 'controller' => '\Inertia\Controller'], $inertiaRoute->action); + $this->assertEquals(['component' => 'User/Edit', 'props' => ['user' => ['name' => 'Jonathan']]], $inertiaRoute->defaults); } } diff --git a/tests/Stubs/CustomUrlResolverMiddleware.php b/tests/Stubs/CustomUrlResolverMiddleware.php new file mode 100644 index 00000000..f690b57b --- /dev/null +++ b/tests/Stubs/CustomUrlResolverMiddleware.php @@ -0,0 +1,21 @@ +set('inertia.testing.page_paths', [realpath(__DIR__)]); } - /** - * @throws LogicException - */ - protected function getTestResponseClass(): string - { - // Laravel >= 7.0 - if (class_exists(TestResponse::class)) { - return TestResponse::class; - } - - // Laravel <= 6.0 - if (class_exists(LegacyTestResponse::class)) { - return LegacyTestResponse::class; - } - - throw new LogicException('Could not detect TestResponse class.'); - } - - /** @returns TestResponse|LegacyTestResponse */ - protected function makeMockRequest($view) + protected function makeMockRequest($view): TestResponse { app('router')->get('/example-url', function () use ($view) { - return $view; + return is_callable($view) ? $view() : $view; }); return $this->get('/example-url'); diff --git a/tests/Testing/AssertableInertiaTest.php b/tests/Testing/AssertableInertiaTest.php index 613cccc2..2d385034 100644 --- a/tests/Testing/AssertableInertiaTest.php +++ b/tests/Testing/AssertableInertiaTest.php @@ -3,13 +3,13 @@ namespace Inertia\Tests\Testing; use Inertia\Inertia; +use Inertia\Testing\AssertableInertia; use Inertia\Tests\TestCase; use PHPUnit\Framework\AssertionFailedError; class AssertableInertiaTest extends TestCase { - /** @test */ - public function the_view_is_served_by_inertia(): void + public function test_the_view_is_served_by_inertia(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -18,8 +18,7 @@ public function the_view_is_served_by_inertia(): void $response->assertInertia(); } - /** @test */ - public function the_view_is_not_served_by_inertia(): void + public function test_the_view_is_not_served_by_inertia(): void { $response = $this->makeMockRequest(view('welcome')); $response->assertOk(); // Make sure we can render the built-in Orchestra 'welcome' view.. @@ -30,8 +29,7 @@ public function the_view_is_not_served_by_inertia(): void $response->assertInertia(); } - /** @test */ - public function the_component_matches(): void + public function test_the_component_matches(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -42,8 +40,7 @@ public function the_component_matches(): void }); } - /** @test */ - public function the_component_does_not_match(): void + public function test_the_component_does_not_match(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -57,8 +54,7 @@ public function the_component_does_not_match(): void }); } - /** @test */ - public function the_component_exists_on_the_filesystem(): void + public function test_the_component_exists_on_the_filesystem(): void { $response = $this->makeMockRequest( Inertia::render('Stubs/ExamplePage') @@ -70,8 +66,7 @@ public function the_component_exists_on_the_filesystem(): void }); } - /** @test */ - public function the_component_does_not_exist_on_the_filesystem(): void + public function test_the_component_does_not_exist_on_the_filesystem(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -86,8 +81,7 @@ public function the_component_does_not_exist_on_the_filesystem(): void }); } - /** @test */ - public function it_can_force_enable_the_component_file_existence(): void + public function test_it_can_force_enable_the_component_file_existence(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -102,8 +96,7 @@ public function it_can_force_enable_the_component_file_existence(): void }); } - /** @test */ - public function it_can_force_disable_the_component_file_existence_check(): void + public function test_it_can_force_disable_the_component_file_existence_check(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -116,8 +109,7 @@ public function it_can_force_disable_the_component_file_existence_check(): void }); } - /** @test */ - public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_exist_relative_to_any_of_the_given_paths(): void + public function test_the_component_does_not_exist_on_the_filesystem_when_it_does_not_exist_relative_to_any_of_the_given_paths(): void { $response = $this->makeMockRequest( Inertia::render('fixtures/ExamplePage') @@ -133,8 +125,7 @@ public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_ }); } - /** @test */ - public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_have_one_of_the_configured_extensions(): void + public function test_the_component_does_not_exist_on_the_filesystem_when_it_does_not_have_one_of_the_configured_extensions(): void { $response = $this->makeMockRequest( Inertia::render('fixtures/ExamplePage') @@ -150,8 +141,7 @@ public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_ }); } - /** @test */ - public function the_page_url_matches(): void + public function test_the_page_url_matches(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -162,8 +152,7 @@ public function the_page_url_matches(): void }); } - /** @test */ - public function the_page_url_does_not_match(): void + public function test_the_page_url_does_not_match(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -177,8 +166,7 @@ public function the_page_url_does_not_match(): void }); } - /** @test */ - public function the_asset_version_matches(): void + public function test_the_asset_version_matches(): void { Inertia::version('example-version'); @@ -191,8 +179,7 @@ public function the_asset_version_matches(): void }); } - /** @test */ - public function the_asset_version_does_not_match(): void + public function test_the_asset_version_does_not_match(): void { Inertia::version('example-version'); @@ -207,4 +194,86 @@ public function the_asset_version_does_not_match(): void $inertia->version('different-version'); }); } + + public function test_reloading_a_visit(): void + { + $foo = 0; + + $response = $this->makeMockRequest(function () use (&$foo) { + return Inertia::render('foo', [ + 'foo' => $foo++, + ]); + }); + + $called = false; + + $response->assertInertia(function ($inertia) use (&$called) { + $inertia->where('foo', 0); + + $inertia->reload(function ($inertia) use (&$called) { + $inertia->where('foo', 1); + $called = true; + }); + }); + + $this->assertTrue($called); + } + + public function test_lazy_props_can_be_evaluated(): void + { + $response = $this->makeMockRequest( + Inertia::render('foo', [ + 'foo' => 'bar', + 'lazy1' => Inertia::lazy(fn () => 'baz'), + 'lazy2' => Inertia::lazy(fn () => 'qux'), + ]) + ); + + $called = false; + + $response->assertInertia(function ($inertia) use (&$called) { + $inertia->where('foo', 'bar'); + $inertia->missing('lazy1'); + $inertia->missing('lazy2'); + + $result = $inertia->reloadOnly('lazy1', function ($inertia) use (&$called) { + $inertia->missing('foo'); + $inertia->where('lazy1', 'baz'); + $inertia->missing('lazy2'); + $called = true; + }); + + $this->assertSame($result, $inertia); + }); + + $this->assertTrue($called); + } + + public function test_lazy_props_can_be_evaluated_with_except(): void + { + $response = $this->makeMockRequest( + Inertia::render('foo', [ + 'foo' => 'bar', + 'lazy1' => Inertia::lazy(fn () => 'baz'), + 'lazy2' => Inertia::lazy(fn () => 'qux'), + ]) + ); + + $called = false; + + $response->assertInertia(function (AssertableInertia $inertia) use (&$called) { + $inertia->where('foo', 'bar'); + $inertia->missing('lazy1'); + $inertia->missing('lazy2'); + + $inertia->reloadExcept('lazy1', function ($inertia) use (&$called) { + $inertia->where('foo', 'bar'); + $inertia->missing('lazy1'); + $inertia->where('lazy2', 'qux'); + $called = true; + }); + }); + + $this->assertTrue($called); + } } diff --git a/tests/Testing/TestResponseMacrosTest.php b/tests/Testing/TestResponseMacrosTest.php index c86fe0de..ed2e7764 100644 --- a/tests/Testing/TestResponseMacrosTest.php +++ b/tests/Testing/TestResponseMacrosTest.php @@ -2,14 +2,14 @@ namespace Inertia\Tests\Testing; +use Illuminate\Testing\Fluent\AssertableJson; +use Illuminate\Testing\TestResponse; use Inertia\Inertia; use Inertia\Tests\TestCase; -use Illuminate\Testing\Fluent\AssertableJson; class TestResponseMacrosTest extends TestCase { - /** @test */ - public function it_can_make_inertia_assertions(): void + public function test_it_can_make_inertia_assertions(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -24,21 +24,19 @@ public function it_can_make_inertia_assertions(): void $this->assertTrue($success); } - /** @test */ - public function it_preserves_the_ability_to_continue_chaining_laravel_test_response_calls(): void + public function test_it_preserves_the_ability_to_continue_chaining_laravel_test_response_calls(): void { $response = $this->makeMockRequest( Inertia::render('foo') ); $this->assertInstanceOf( - $this->getTestResponseClass(), + TestResponse::class, $response->assertInertia() ); } - /** @test */ - public function it_can_retrieve_the_inertia_page(): void + public function test_it_can_retrieve_the_inertia_page(): void { $response = $this->makeMockRequest( Inertia::render('foo', ['bar' => 'baz']) @@ -49,6 +47,34 @@ public function it_can_retrieve_the_inertia_page(): void $this->assertSame(['bar' => 'baz'], $page['props']); $this->assertSame('/example-url', $page['url']); $this->assertSame('', $page['version']); + $this->assertFalse($page['encryptHistory']); + $this->assertFalse($page['clearHistory']); }); } + + public function test_it_can_retrieve_the_inertia_props(): void + { + $props = ['bar' => 'baz']; + $response = $this->makeMockRequest( + Inertia::render('foo', $props) + ); + + $this->assertSame($props, $response->inertiaProps()); + } + + public function test_it_can_retrieve_nested_inertia_prop_values_with_dot_notation(): void + { + $response = $this->makeMockRequest( + Inertia::render('foo', [ + 'bar' => ['baz' => 'qux'], + 'users' => [ + ['name' => 'John'], + ['name' => 'Jane'], + ], + ]) + ); + + $this->assertSame('qux', $response->inertiaProps('bar.baz')); + $this->assertSame('John', $response->inertiaProps('users.0.name')); + } }