├── src ├── Concerns │ ├── Debugging.php │ ├── Interaction.php │ ├── PageObject.php │ ├── Matching.php │ └── Has.php ├── TestResponseMacros.php ├── InertiaTestingServiceProvider.php └── Assert.php ├── _ide_helpers.php ├── LICENSE ├── composer.json └── config └── inertia.php /src/Concerns/Debugging.php: -------------------------------------------------------------------------------- 1 | prop($prop)); 10 | 11 | return $this; 12 | } 13 | 14 | public function dd(string $prop = null): void 15 | { 16 | dd($this->prop($prop)); 17 | } 18 | 19 | abstract protected function prop(string $key = null); 20 | } 21 | -------------------------------------------------------------------------------- /_ide_helpers.php: -------------------------------------------------------------------------------- 1 | interacted(); 22 | } 23 | 24 | return $this; 25 | }; 26 | } 27 | 28 | public function inertiaPage() 29 | { 30 | return function () { 31 | return Assert::fromTestResponse($this)->toArray(); 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Claudio Dekker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Concerns/Interaction.php: -------------------------------------------------------------------------------- 1 | interacted, true)) { 18 | $this->interacted[] = $prop; 19 | } 20 | } 21 | 22 | public function interacted(): void 23 | { 24 | PHPUnit::assertSame( 25 | [], 26 | array_diff(array_keys($this->prop()), $this->interacted), 27 | $this->path 28 | ? sprintf('Unexpected Inertia properties were found in scope [%s].', $this->path) 29 | : 'Unexpected Inertia properties were found on the root level.' 30 | ); 31 | } 32 | 33 | public function etc(): self 34 | { 35 | $this->interacted = array_keys($this->prop()); 36 | 37 | return $this; 38 | } 39 | 40 | abstract protected function prop(string $key = null); 41 | } 42 | -------------------------------------------------------------------------------- /src/Concerns/PageObject.php: -------------------------------------------------------------------------------- 1 | component, 'Unexpected Inertia page component.'); 14 | 15 | if ($shouldExist || (is_null($shouldExist) && config('inertia.page.should_exist', true))) { 16 | try { 17 | app('inertia.view.finder')->find($value); 18 | } catch (InvalidArgumentException $exception) { 19 | PHPUnit::fail(sprintf('Inertia page component file [%s] does not exist.', $value)); 20 | } 21 | } 22 | 23 | return $this; 24 | } 25 | 26 | protected function prop(string $key = null) 27 | { 28 | return Arr::get($this->props, $key); 29 | } 30 | 31 | public function url(string $value): self 32 | { 33 | PHPUnit::assertSame($value, $this->url, 'Unexpected Inertia page url.'); 34 | 35 | return $this; 36 | } 37 | 38 | public function version($value): self 39 | { 40 | PHPUnit::assertSame($value, $this->version, 'Unexpected Inertia asset version.'); 41 | 42 | return $this; 43 | } 44 | 45 | public function toArray(): array 46 | { 47 | return [ 48 | 'component' => $this->component, 49 | 'props' => $this->props, 50 | 'url' => $this->url, 51 | 'version' => $this->version, 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claudiodekker/inertia-laravel-testing", 3 | "description": "Testing helpers for https://github.com/inertiajs/inertia-laravel", 4 | "keywords": [ 5 | "claudiodekker", 6 | "laravel", 7 | "inertia", 8 | "testing", 9 | "helpers", 10 | "phpunit" 11 | ], 12 | "homepage": "https://github.com/claudiodekker/inertia-laravel-testing", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Claudio Dekker", 17 | "email": "claudio@ubient.net", 18 | "homepage": "https://dekker.io", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.2|^8.0", 24 | "ext-json": "*", 25 | "inertiajs/inertia-laravel": "^v0.3", 26 | "laravel/framework": "^6.0|^7.0|^8.0" 27 | }, 28 | "require-dev": { 29 | "roave/security-advisories": "dev-master", 30 | "orchestra/testbench": "^4.0|^5.0|^6.0", 31 | "phpunit/phpunit": "^8.0|^9.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "ClaudioDekker\\Inertia\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "ClaudioDekker\\Inertia\\Tests\\": "tests" 41 | } 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "ClaudioDekker\\Inertia\\InertiaTestingServiceProvider" 47 | ] 48 | } 49 | }, 50 | "scripts": { 51 | "test": "phpunit" 52 | }, 53 | "config": { 54 | "sort-packages": true 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /config/inertia.php: -------------------------------------------------------------------------------- 1 | false, 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Page 23 | |-------------------------------------------------------------------------- 24 | | 25 | | The values described here are used to locate Inertia components on the 26 | | filesystem. For instance, when using `assertInertia`, the assertion 27 | | attempts to locate the component as a file relative to any of the 28 | | paths AND with any of the extensions specified here. 29 | | 30 | */ 31 | 32 | 'page' => [ 33 | 34 | /** 35 | * Determines whether assertions should check that Inertia page components 36 | * actually exist on the filesystem instead of just checking responses. 37 | */ 38 | 'should_exist' => true, 39 | 40 | /* 41 | * A list of root paths to your Inertia page components. 42 | */ 43 | 'paths' => [ 44 | 45 | resource_path('js/Pages'), 46 | 47 | ], 48 | 49 | /* 50 | * A list of valid Inertia page component extensions. 51 | */ 52 | 'extensions' => [ 53 | 54 | 'vue', 55 | 'svelte', 56 | 57 | ], 58 | 59 | ], 60 | 61 | ]; 62 | -------------------------------------------------------------------------------- /src/Concerns/Matching.php: -------------------------------------------------------------------------------- 1 | $value) { 16 | $this->where($key, $value); 17 | } 18 | 19 | return $this; 20 | } 21 | 22 | public function where($key, $expected): self 23 | { 24 | $this->has($key); 25 | 26 | $actual = $this->prop($key); 27 | 28 | if ($expected instanceof Closure) { 29 | PHPUnit::assertTrue( 30 | $expected(is_array($actual) ? Collection::make($actual) : $actual), 31 | sprintf('Inertia property [%s] was marked as invalid using a closure.', $this->dotPath($key)) 32 | ); 33 | 34 | return $this; 35 | } 36 | 37 | if ($expected instanceof Arrayable) { 38 | $expected = $expected->toArray(); 39 | } elseif ($expected instanceof Responsable) { 40 | $expected = json_decode(json_encode($expected->toResponse(request())->getData()), true); 41 | } 42 | 43 | $this->ensureSorted($expected); 44 | $this->ensureSorted($actual); 45 | 46 | PHPUnit::assertSame( 47 | $expected, 48 | $actual, 49 | sprintf('Inertia property [%s] does not match the expected value.', $this->dotPath($key)) 50 | ); 51 | 52 | return $this; 53 | } 54 | 55 | protected function ensureSorted(&$value): void 56 | { 57 | if (! is_array($value)) { 58 | return; 59 | } 60 | 61 | foreach ($value as &$arg) { 62 | $this->ensureSorted($arg); 63 | } 64 | 65 | ksort($value); 66 | } 67 | 68 | abstract protected function dotPath($key): string; 69 | 70 | abstract protected function prop(string $key = null); 71 | 72 | abstract public function has(string $key, $value = null, Closure $scope = null); 73 | } 74 | -------------------------------------------------------------------------------- /src/InertiaTestingServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerTestingMacros(); 25 | } 26 | 27 | $this->publishes([ 28 | __DIR__.'/../config/inertia.php' => config_path('inertia.php'), 29 | ]); 30 | } 31 | 32 | public function register() 33 | { 34 | // When the installed Inertia adapter has our assertions bundled, 35 | // we'll skip registering and/or booting this package. 36 | if (class_exists(InertiaAssertions::class)) { 37 | return; 38 | } 39 | 40 | $this->mergeConfigFrom( 41 | __DIR__.'/../config/inertia.php', 42 | 'inertia' 43 | ); 44 | 45 | $this->app->bind('inertia.view.finder', function ($app) { 46 | return new FileViewFinder( 47 | $app['files'], 48 | $app['config']->get('inertia.page.paths'), 49 | $app['config']->get('inertia.page.extensions') 50 | ); 51 | }); 52 | } 53 | 54 | protected function registerTestingMacros() 55 | { 56 | // Laravel >= 7.0 57 | if (class_exists(TestResponse::class)) { 58 | TestResponse::mixin(new TestResponseMacros()); 59 | 60 | return; 61 | } 62 | 63 | // Laravel <= 6.0 64 | if (class_exists(LegacyTestResponse::class)) { 65 | LegacyTestResponse::mixin(new TestResponseMacros()); 66 | 67 | return; 68 | } 69 | 70 | throw new LogicException('Could not detect TestResponse class.'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Assert.php: -------------------------------------------------------------------------------- 1 | path = $path; 38 | 39 | $this->component = $component; 40 | $this->props = $props; 41 | $this->url = $url; 42 | $this->version = $version; 43 | } 44 | 45 | protected function dotPath($key): string 46 | { 47 | if (is_null($this->path)) { 48 | return $key; 49 | } 50 | 51 | return implode('.', [$this->path, $key]); 52 | } 53 | 54 | protected function scope($key, Closure $callback): self 55 | { 56 | $props = $this->prop($key); 57 | $path = $this->dotPath($key); 58 | 59 | PHPUnit::assertIsArray($props, sprintf('Inertia property [%s] is not scopeable.', $path)); 60 | 61 | $scope = new self($this->component, $props, $this->url, $this->version, $path); 62 | $callback($scope); 63 | $scope->interacted(); 64 | 65 | return $this; 66 | } 67 | 68 | public static function fromTestResponse($response): self 69 | { 70 | try { 71 | $response->assertViewHas('page'); 72 | $page = json_decode(json_encode($response->viewData('page')), true); 73 | 74 | PHPUnit::assertIsArray($page); 75 | PHPUnit::assertArrayHasKey('component', $page); 76 | PHPUnit::assertArrayHasKey('props', $page); 77 | PHPUnit::assertArrayHasKey('url', $page); 78 | PHPUnit::assertArrayHasKey('version', $page); 79 | } catch (AssertionFailedError $e) { 80 | PHPUnit::fail('Not a valid Inertia response.'); 81 | } 82 | 83 | return new self($page['component'], $page['props'], $page['url'], $page['version']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Concerns/Has.php: -------------------------------------------------------------------------------- 1 | prop($key), 17 | sprintf('Inertia property [%s] does not have the expected size.', $this->dotPath($key)) 18 | ); 19 | 20 | return $this; 21 | } 22 | 23 | public function hasAll($key): self 24 | { 25 | $keys = is_array($key) ? $key : func_get_args(); 26 | 27 | foreach ($keys as $prop => $count) { 28 | if (is_int($prop)) { 29 | $this->has($count); 30 | } else { 31 | $this->has($prop, $count); 32 | } 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | public function has(string $key, $value = null, Closure $scope = null): self 39 | { 40 | PHPUnit::assertTrue( 41 | Arr::has($this->prop(), $key), 42 | sprintf('Inertia property [%s] does not exist.', $this->dotPath($key)) 43 | ); 44 | 45 | $this->interactsWith($key); 46 | 47 | if (is_int($value) && ! is_null($scope)) { 48 | $path = $this->dotPath($key); 49 | 50 | $prop = $this->prop($key); 51 | if ($prop instanceof Collection) { 52 | $prop = $prop->all(); 53 | } 54 | 55 | PHPUnit::assertTrue($value > 0, sprintf('Cannot scope directly onto the first entry of property [%s] when asserting that it has a size of 0.', $path)); 56 | PHPUnit::assertIsArray($prop, sprintf('Direct scoping is currently unsupported for non-array like properties such as [%s].', $path)); 57 | $this->count($key, $value); 58 | 59 | return $this->scope($key.'.'.array_keys($prop)[0], $scope); 60 | } 61 | 62 | if (is_callable($value)) { 63 | $this->scope($key, $value); 64 | } elseif (! is_null($value)) { 65 | $this->count($key, $value); 66 | } 67 | 68 | return $this; 69 | } 70 | 71 | public function missingAll($key): self 72 | { 73 | $keys = is_array($key) ? $key : func_get_args(); 74 | 75 | foreach ($keys as $prop) { 76 | $this->misses($prop); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | public function missing(string $key): self 83 | { 84 | $this->interactsWith($key); 85 | 86 | PHPUnit::assertNotTrue( 87 | Arr::has($this->prop(), $key), 88 | sprintf('Inertia property [%s] was found while it was expected to be missing.', $this->dotPath($key)) 89 | ); 90 | 91 | return $this; 92 | } 93 | 94 | public function missesAll($key): self 95 | { 96 | return $this->missingAll( 97 | is_array($key) ? $key : func_get_args() 98 | ); 99 | } 100 | 101 | public function misses(string $key): self 102 | { 103 | return $this->missing($key); 104 | } 105 | 106 | abstract protected function prop(string $key = null); 107 | 108 | abstract protected function dotPath($key): string; 109 | 110 | abstract protected function interactsWith(string $key): void; 111 | 112 | abstract protected function scope($key, Closure $callback); 113 | } 114 | --------------------------------------------------------------------------------