From 295827f1ab2020d867d0748b6298fc08f6a35fa2 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 9 Jan 2026 17:41:09 +0100 Subject: [PATCH 1/4] wip new testing API --- .../EntityList/AssertableEntityList.php | 19 +++++ .../Testing/EntityList/PendingEntityList.php | 53 +++++++++++++ src/Utils/Testing/GeneratesSharpUrl.php | 64 ++++++++++++++++ src/Utils/Testing/SharpAssertions.php | 76 +++++-------------- .../Utils/Testing/SharpAssertionsTest.php | 59 +++++++++++++- 5 files changed, 210 insertions(+), 61 deletions(-) create mode 100644 src/Utils/Testing/EntityList/AssertableEntityList.php create mode 100644 src/Utils/Testing/EntityList/PendingEntityList.php create mode 100644 src/Utils/Testing/GeneratesSharpUrl.php diff --git a/src/Utils/Testing/EntityList/AssertableEntityList.php b/src/Utils/Testing/EntityList/AssertableEntityList.php new file mode 100644 index 000000000..fd14249ea --- /dev/null +++ b/src/Utils/Testing/EntityList/AssertableEntityList.php @@ -0,0 +1,19 @@ +response->assertOk(); + + return $this; + } +} diff --git a/src/Utils/Testing/EntityList/PendingEntityList.php b/src/Utils/Testing/EntityList/PendingEntityList.php new file mode 100644 index 000000000..f1a4dc0ea --- /dev/null +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -0,0 +1,53 @@ +entityKeyFor($this->entityKey); + $this->entityList = app(SharpEntityManager::class)->entityFor($resolvedEntityKey)->getListOrFail(); + } + + public function withFilter(string $filterKey, mixed $value): static + { + $key = $this->entityList->filterContainer()->findFilterHandler($filterKey)->getKey(); + $this->filterValues[$key] = $value; + + return $this; + } + + public function get(): AssertableEntityList + { + $this->setGlobalFilterUrlDefault(); + + return new AssertableEntityList( + $this->test->get( + route('code16.sharp.list', [ + 'entityKey' => $this->entityKey, + ...$this->entityList + ->filterContainer() + ->getQueryParamsFromFilterValues($this->filterValues), + ]) + ) + ); + } + + public function callInstanceCommand() {} + + public function callEntityCommand() {} +} diff --git a/src/Utils/Testing/GeneratesSharpUrl.php b/src/Utils/Testing/GeneratesSharpUrl.php new file mode 100644 index 000000000..6fb632bdf --- /dev/null +++ b/src/Utils/Testing/GeneratesSharpUrl.php @@ -0,0 +1,64 @@ +breadcrumbBuilder = $callback(new BreadcrumbBuilder()); + + return $this; + } + + public function withSharpGlobalFilterValues(array|string $globalFilterValues): self + { + $this->globalFilter = collect((array) $globalFilterValues) + ->implode(GlobalFilters::$valuesUrlSeparator); + + return $this; + } + + private function setGlobalFilterUrlDefault(): void + { + URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + } + + private function breadcrumbBuilder(string $entityKey, ?string $instanceId = null): BreadcrumbBuilder + { + if (isset($this->breadcrumbBuilder)) { + return $this->breadcrumbBuilder; + } + + return (new BreadcrumbBuilder()) + ->appendEntityList($entityKey) + ->when($instanceId, fn ($builder) => $builder->appendShowPage($entityKey, $instanceId)); + } + + private function buildCurrentPageUrl(BreadcrumbBuilder $builder): string + { + return url( + sprintf( + '/%s/%s/%s', + sharp()->config()->get('custom_url_segment'), + sharp()->context()->globalFilterUrlSegmentValue(), + $builder->generateUri() + ) + ); + } +} diff --git a/src/Utils/Testing/SharpAssertions.php b/src/Utils/Testing/SharpAssertions.php index 1e1bf980b..a22318763 100644 --- a/src/Utils/Testing/SharpAssertions.php +++ b/src/Utils/Testing/SharpAssertions.php @@ -2,17 +2,19 @@ namespace Code16\Sharp\Utils\Testing; -use Closure; -use Code16\Sharp\Filters\GlobalFilters\GlobalFilters; use Code16\Sharp\Http\Context\SharpBreadcrumb; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Links\BreadcrumbBuilder; -use Illuminate\Support\Facades\URL; +use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; trait SharpAssertions { - private BreadcrumbBuilder $breadcrumbBuilder; - private ?string $globalFilter = null; + use GeneratesSharpUrl; + + public function sharpList(string $entityClassNameOrKey): PendingEntityList + { + return new PendingEntityList($this, $entityClassNameOrKey); + } /** * @deprecated use withSharpBreadcrumb() instead @@ -32,28 +34,9 @@ public function withSharpCurrentBreadcrumb(...$breadcrumb): self return $this; } - /** - * @param (\Closure(BreadcrumbBuilder): BreadcrumbBuilder) $callback - * @return $this - */ - public function withSharpBreadcrumb(Closure $callback): self - { - $this->breadcrumbBuilder = $callback(new BreadcrumbBuilder()); - - return $this; - } - - public function withSharpGlobalFilterValues(array|string $globalFilterValues): self - { - $this->globalFilter = collect((array) $globalFilterValues) - ->implode(GlobalFilters::$valuesUrlSeparator); - - return $this; - } - public function deleteFromSharpShow(string $entityClassNameOrKey, mixed $instanceId) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -72,7 +55,7 @@ public function deleteFromSharpShow(string $entityClassNameOrKey, mixed $instanc public function deleteFromSharpList(string $entityClassNameOrKey, mixed $instanceId) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -94,7 +77,7 @@ public function deleteFromSharpList(string $entityClassNameOrKey, mixed $instanc public function getSharpForm(string $entityClassNameOrKey, mixed $instanceId = null) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -123,7 +106,7 @@ public function getSharpForm(string $entityClassNameOrKey, mixed $instanceId = n public function getSharpSingleForm(string $entityClassNameOrKey) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -142,7 +125,7 @@ public function getSharpSingleForm(string $entityClassNameOrKey) public function updateSharpForm(string $entityClassNameOrKey, $instanceId, array $data) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -162,7 +145,7 @@ public function updateSharpForm(string $entityClassNameOrKey, $instanceId, array public function updateSharpSingleForm(string $entityClassNameOrKey, array $data) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -181,7 +164,7 @@ public function updateSharpSingleForm(string $entityClassNameOrKey, array $data) public function getSharpShow(string $entityClassNameOrKey, $instanceId) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -201,7 +184,7 @@ public function getSharpShow(string $entityClassNameOrKey, $instanceId) public function storeSharpForm(string $entityClassNameOrKey, array $data) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -226,7 +209,7 @@ public function callSharpInstanceCommandFromList( array $data = [], ?string $commandStep = null ) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -257,7 +240,7 @@ public function callSharpInstanceCommandFromShow( array $data = [], ?string $commandStep = null ) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -287,7 +270,7 @@ public function callSharpEntityCommandFromList( array $data = [], ?string $commandStep = null ) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -311,29 +294,6 @@ public function loginAsSharpUser($user): self return $this->actingAs($user, sharp()->config()->get('auth.guard') ?: config('auth.defaults.guard')); } - private function breadcrumbBuilder(string $entityKey, ?string $instanceId = null): BreadcrumbBuilder - { - if (isset($this->breadcrumbBuilder)) { - return $this->breadcrumbBuilder; - } - - return (new BreadcrumbBuilder()) - ->appendEntityList($entityKey) - ->when($instanceId, fn ($builder) => $builder->appendShowPage($entityKey, $instanceId)); - } - - private function buildCurrentPageUrl(BreadcrumbBuilder $builder): string - { - return url( - sprintf( - '/%s/%s/%s', - sharp()->config()->get('custom_url_segment'), - sharp()->context()->globalFilterUrlSegmentValue(), - $builder->generateUri() - ) - ); - } - private function resolveEntityKey(string $entityClassNameOrKey): string { return app(SharpEntityManager::class)->entityKeyFor($entityClassNameOrKey); diff --git a/tests/Unit/Utils/Testing/SharpAssertionsTest.php b/tests/Unit/Utils/Testing/SharpAssertionsTest.php index 64b2fc166..a317e0c81 100644 --- a/tests/Unit/Utils/Testing/SharpAssertionsTest.php +++ b/tests/Unit/Utils/Testing/SharpAssertionsTest.php @@ -1,9 +1,55 @@ config()->declareEntity(PersonEntity::class); +}); + +it('allows to test entity list', function () { + fakeListFor(PersonEntity::class, new class() extends PersonList + { + public function getFilters(): ?array + { + return [ + new class() extends SelectFilter + { + public function buildFilterConfig(): void + { + $this->configureKey('job'); + } + + public function values(): array + { + return [ + 'physicist' => 'Physicist', + 'physician' => 'Physician', + ]; + } + }, + ]; + } + }); + + $response = fakeResponse(); + $response->sharpList('person') + ->withFilter('job', 'physicist') + ->get(); + + expect($response->uri)->toEqual( + route('code16.sharp.list', [ + 'entityKey' => 'person', + 'filter_job' => 'physicist', + ]) + ); +}); + it('allows to test getSharpShow', function () { $response = fakeResponse()->getSharpShow('leaves', 6); @@ -249,9 +295,10 @@ function fakeResponse() return new class('fake') extends Orchestra\Testbench\TestCase { use SharpAssertions; + use Tappable; - public $uri; - public $postedData; + public string $uri; + public mixed $postedData; public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null) { @@ -265,7 +312,13 @@ public function call($method, $uri, $parameters = [], $cookies = [], $files = [] $this->postedData = null; } - return $this; + return new class($this->uri, $this->postedData) extends \Illuminate\Testing\TestResponse + { + public function __construct(public $uri, public $postedData) + { + parent::__construct(new \Illuminate\Http\Response()); + } + }; } }; } From 904345f55cff4eef09bdc3e83c6a029f0255f243 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2026 21:04:52 +0100 Subject: [PATCH 2/4] wip --- .../Testing/Commands/AssertableCommand.php | 71 ++++ src/Utils/Testing/DelegatesToResponse.php | 22 ++ .../EntityList/AssertableEntityList.php | 27 +- .../Testing/EntityList/PendingEntityList.php | 94 ++++- tests/Http/SharpAssertionsHttpTest.php | 72 ++++ .../Utils/Testing/SharpAssertionsTest.php | 342 ++++-------------- .../Utils/Testing/SharpAssertionsTestCase.php | 11 + .../Testing/SharpLegacyAssertionsTest.php | 269 ++++++++++++++ 8 files changed, 617 insertions(+), 291 deletions(-) create mode 100644 src/Utils/Testing/Commands/AssertableCommand.php create mode 100644 src/Utils/Testing/DelegatesToResponse.php create mode 100644 tests/Http/SharpAssertionsHttpTest.php create mode 100644 tests/Unit/Utils/Testing/SharpAssertionsTestCase.php create mode 100644 tests/Unit/Utils/Testing/SharpLegacyAssertionsTest.php diff --git a/src/Utils/Testing/Commands/AssertableCommand.php b/src/Utils/Testing/Commands/AssertableCommand.php new file mode 100644 index 000000000..ab8570f30 --- /dev/null +++ b/src/Utils/Testing/Commands/AssertableCommand.php @@ -0,0 +1,71 @@ +response->assertJson(fn (AssertableJson $json) => $json->where('action', 'html') + ); + + return $this; + } + + public function assertReturnsInfo(string $message = ''): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'info') + ->when($message)->where('message', $message) + ->etc() + ); + + return $this; + } + + public function assertReturnsLink(string $url = ''): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'link') + ->when($url)->where('url', $url) + ); + + return $this; + } + + public function assertReturnsReload(): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'reload') + ); + + return $this; + } + + public function assertReturnsRefresh(): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'refresh') + ); + + return $this; + } + + public function assertReturnsStep(string $step): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'step') + ); + + PHPUnit::assertEquals($step, Str::before($this->response->json('step'), ':')); + + return $this; + } +} diff --git a/src/Utils/Testing/DelegatesToResponse.php b/src/Utils/Testing/DelegatesToResponse.php new file mode 100644 index 000000000..8d1a6bbdd --- /dev/null +++ b/src/Utils/Testing/DelegatesToResponse.php @@ -0,0 +1,22 @@ +response->{$name}(...$arguments); + + return $this; + } +} diff --git a/src/Utils/Testing/EntityList/AssertableEntityList.php b/src/Utils/Testing/EntityList/AssertableEntityList.php index fd14249ea..e703cb02c 100644 --- a/src/Utils/Testing/EntityList/AssertableEntityList.php +++ b/src/Utils/Testing/EntityList/AssertableEntityList.php @@ -2,18 +2,41 @@ namespace Code16\Sharp\Utils\Testing\EntityList; +use Code16\Sharp\Utils\Testing\DelegatesToResponse; use Illuminate\Testing\TestResponse; +use PHPUnit\Framework\Assert as PHPUnit; class AssertableEntityList { + use DelegatesToResponse; + public function __construct( protected TestResponse $response, ) {} - public function assertOk(): self + public function assertListCount(int $count): self { - $this->response->assertOk(); + PHPUnit::assertCount($count, $this->listData()); return $this; } + + public function assertListContains(array $attributes): self + { + PHPUnit::assertTrue( + collect($this->listData())->contains(fn ($item) => collect($attributes)->every(fn ($value, $key) => isset($item[$key]) && $item[$key] === $value) + ), + sprintf( + 'Failed asserting that data contains an item with attributes: %s', + json_encode($attributes) + ) + ); + + return $this; + } + + protected function listData(): array + { + return $this->response->inertiaProps('entityList.data'); + } } diff --git a/src/Utils/Testing/EntityList/PendingEntityList.php b/src/Utils/Testing/EntityList/PendingEntityList.php index f1a4dc0ea..f17039418 100644 --- a/src/Utils/Testing/EntityList/PendingEntityList.php +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -3,9 +3,11 @@ namespace Code16\Sharp\Utils\Testing\EntityList; use Code16\Sharp\EntityList\SharpEntityList; +use Code16\Sharp\Http\Context\SharpBreadcrumb; use Code16\Sharp\Utils\Entities\SharpEntityManager; +use Code16\Sharp\Utils\Testing\Commands\AssertableCommand; use Code16\Sharp\Utils\Testing\GeneratesSharpUrl; -use Illuminate\Foundation\Testing\Concerns\MakesHttpRequests; +use Illuminate\Foundation\Testing\TestCase; class PendingEntityList { @@ -13,14 +15,15 @@ class PendingEntityList protected SharpEntityList $entityList; protected array $filterValues = []; + protected string $entityKey; public function __construct( - /** @var MakesHttpRequests $test */ + /** @var TestCase $test */ protected object $test, - protected string $entityKey + string $entityKey ) { - $resolvedEntityKey = app(SharpEntityManager::class)->entityKeyFor($this->entityKey); - $this->entityList = app(SharpEntityManager::class)->entityFor($resolvedEntityKey)->getListOrFail(); + $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); + $this->entityList = app(SharpEntityManager::class)->entityFor($this->entityKey)->getListOrFail(); } public function withFilter(string $filterKey, mixed $value): static @@ -39,15 +42,86 @@ public function get(): AssertableEntityList $this->test->get( route('code16.sharp.list', [ 'entityKey' => $this->entityKey, - ...$this->entityList - ->filterContainer() - ->getQueryParamsFromFilterValues($this->filterValues), + ...$this->entityListQueryParams(), ]) ) ); } - public function callInstanceCommand() {} + public function callEntityCommand( + string $commandKeyOrClassName, + array $data = [], + ?string $commandStep = null + ): AssertableCommand { + $this->setGlobalFilterUrlDefault(); + + $commandKey = class_exists($commandKeyOrClassName) + ? class_basename($commandKeyOrClassName) + : $commandKeyOrClassName; + + return new AssertableCommand( + $this + ->test + ->withHeader( + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, + $this->buildCurrentPageUrl( + $this->breadcrumbBuilder($this->entityKey) + ), + ) + ->postJson( + route( + 'code16.sharp.api.list.command.entity', + ['entityKey' => $this->entityKey, 'commandKey' => $commandKey] + ), + [ + 'data' => $data, + 'query' => $this->entityListQueryParams(), + 'command_step' => $commandStep, + ], + ) + ); + } + + public function callInstanceCommand( + int|string $instanceId, + string $commandKeyOrClassName, + array $data = [], + ?string $commandStep = null + ): AssertableCommand { + $this->setGlobalFilterUrlDefault(); - public function callEntityCommand() {} + $commandKey = class_exists($commandKeyOrClassName) + ? class_basename($commandKeyOrClassName) + : $commandKeyOrClassName; + + return new AssertableCommand( + $this + ->test + ->withHeader( + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, + $this->buildCurrentPageUrl( + $this->breadcrumbBuilder($this->entityKey, $instanceId) + ), + ) + ->postJson( + route( + 'code16.sharp.api.list.command.instance', + ['entityKey' => $this->entityKey, 'instanceId' => $instanceId, 'commandKey' => $commandKey] + ), + [ + 'data' => $data, + 'query' => $this->entityListQueryParams(), + 'command_step' => $commandStep, + ], + ) + ); + } + + protected function entityListQueryParams(): array + { + return $this->entityList + ->filterContainer() + ->getQueryParamsFromFilterValues($this->filterValues) + ->all(); + } } diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php new file mode 100644 index 000000000..ca9557374 --- /dev/null +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -0,0 +1,72 @@ +use(SharpAssertions::class); + +beforeEach(function () { + login(); + sharp()->config()->declareEntity(PersonEntity::class); +}); + +it('gets an entity list', function () { + fakeListFor(PersonEntity::class, new class() extends PersonList + { + public function getListData(): array + { + return [ + ['id' => 1, 'name' => 'Marie Curie'], + ]; + } + }); + + $this->sharpList(PersonEntity::class) + ->get() + ->assertOk() + ->assertListCount(1) + ->assertListContains(['name' => 'Marie Curie']); +}); + +it('call an entity list entity command', function () { + fakeListFor('person', new class() extends PersonList + { + protected function getEntityCommands(): ?array + { + return [ + 'cmd' => new class() extends EntityCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('action')); + } + + public function execute(array $data = []): array + { + return match ($data['action']) { + 'info' => $this->info('ok'), + 'link' => $this->link('https://example.org'), + }; + } + }, + ]; + } + }); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'info']) + ->assertReturnsInfo('ok'); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'link']) + ->assertReturnsLink('https://example.org'); +}); diff --git a/tests/Unit/Utils/Testing/SharpAssertionsTest.php b/tests/Unit/Utils/Testing/SharpAssertionsTest.php index a317e0c81..d75e7ee5a 100644 --- a/tests/Unit/Utils/Testing/SharpAssertionsTest.php +++ b/tests/Unit/Utils/Testing/SharpAssertionsTest.php @@ -3,16 +3,16 @@ use Code16\Sharp\Filters\SelectFilter; use Code16\Sharp\Tests\Fixtures\Entities\PersonEntity; use Code16\Sharp\Tests\Fixtures\Sharp\PersonList; +use Code16\Sharp\Tests\Unit\Utils\Testing\SharpAssertionsTestCase; use Code16\Sharp\Utils\Testing\SharpAssertions; -use Illuminate\Support\Traits\Tappable; +use Illuminate\Http\Response; +use Illuminate\Testing\TestResponse; uses(SharpAssertions::class); beforeEach(function () { sharp()->config()->declareEntity(PersonEntity::class); -}); -it('allows to test entity list', function () { fakeListFor(PersonEntity::class, new class() extends PersonList { public function getFilters(): ?array @@ -36,289 +36,73 @@ public function values(): array ]; } }); +}); - $response = fakeResponse(); - $response->sharpList('person') - ->withFilter('job', 'physicist') - ->get(); +it('allows to test entity list', function () { + /** @var \Mockery\MockInterface|SharpAssertionsTestCase $testMock */ + $testMock = Mockery::mock(SharpAssertionsTestCase::class)->makePartial(); - expect($response->uri)->toEqual( - route('code16.sharp.list', [ + $testMock->shouldReceive('get') + ->once() + ->with(route('code16.sharp.list', [ 'entityKey' => 'person', 'filter_job' => 'physicist', - ]) - ); -}); - -it('allows to test getSharpShow', function () { - $response = fakeResponse()->getSharpShow('leaves', 6); - - $this->assertEquals( - route('code16.sharp.show.show', ['s-list/leaves', 'leaves', 6]), - $response->uri, - ); -}); - -it('allows to test getSharpForm for edit', function () { - $response = fakeResponse()->getSharpForm('leaves', 6); - - $this->assertEquals( - route('code16.sharp.form.edit', ['s-list/leaves', 'leaves', 6]), - $response->uri, - ); -}); - -it('allows to test getSharpForm for edit with a custom breadcrumb', function () { - $response = fakeResponse() - ->withSharpBreadcrumb( - fn ($builder) => $builder - ->appendEntityList('leaves') - ->appendShowPage('leaves', 6), - ) - ->getSharpForm('leaves', 6); - - $this->assertEquals( - route('code16.sharp.form.edit', ['s-list/leaves/s-show/leaves/6', 'leaves', 6]), - $response->uri, - ); -}); - -it('allows to test getSharpForm for single edit', function () { - $response = fakeResponse()->getSharpSingleForm('leaves'); - - $this->assertEquals( - route('code16.sharp.form.edit', ['s-list/leaves', 'leaves']), - $response->uri, - ); -}); - -it('allows to test getSharpForm for create', function () { - $response = fakeResponse()->getSharpForm('leaves'); - - $this->assertEquals( - route('code16.sharp.form.create', ['s-list/leaves', 'leaves']), - $response->uri, - ); -}); - -it('allows to test updateSharpForm for update', function () { - $response = fakeResponse()->updateSharpForm('leaves', 6, ['attr' => 'some_value']); - - $this->assertEquals( - route('code16.sharp.form.update', ['s-list/leaves', 'leaves', 6]), - $response->uri, - ); - - $this->assertEquals( - ['attr' => 'some_value'], - $response->postedData, - ); -}); - -it('allows to test updateSharpForm for single update', function () { - $response = fakeResponse() - ->updateSharpSingleForm('leaves', ['attr' => 'some_value']); - - $this->assertEquals( - route('code16.sharp.form.update', ['s-list/leaves', 'leaves']), - $response->uri, - ); - - $this->assertEquals( - ['attr' => 'some_value'], - $response->postedData, - ); -}); - -it('allows to test updateSharpForm for store', function () { - $response = fakeResponse()->storeSharpForm('leaves', ['attr' => 'some_value']); - - $this->assertEquals( - route('code16.sharp.form.store', ['s-list/leaves', 'leaves']), - $response->uri, - ); - - $this->assertEquals( - ['attr' => 'some_value'], - $response->postedData, - ); -}); - -it('allows to test deleteFromSharpList', function () { - $response = fakeResponse()->deleteFromSharpList('leaves', 6); - - $this->assertEquals( - route('code16.sharp.api.list.delete', ['leaves', 6]), - $response->uri, - ); -}); - -it('allows to test deleteSharpShow', function () { - $response = fakeResponse()->deleteFromSharpShow('leaves', 6); + ])) + ->andReturn(new TestResponse(new Response())); - $this->assertEquals( - route('code16.sharp.show.delete', ['s-list/leaves', 'leaves', 6]), - $response->uri, - ); -}); - -it('allows to test callSharpInstanceCommandFromList', function () { - $response = fakeResponse() - ->callSharpInstanceCommandFromList('leaves', 6, 'command', ['attr' => 'some_value']); - - $this->assertEquals( - route('code16.sharp.api.list.command.instance', [ - 'entityKey' => 'leaves', - 'instanceId' => 6, - 'commandKey' => 'command', - ]), - $response->uri, - ); - - $this->assertEquals('some_value', $response->postedData->data->attr); -}); - -it('allows to test callSharpInstanceCommandFromShow', function () { - $response = fakeResponse() - ->callSharpInstanceCommandFromShow('leaves', 6, 'command', ['attr' => 'some_value']); - - $this->assertEquals( - route('code16.sharp.api.show.command.instance', [ - 'entityKey' => 'leaves', - 'instanceId' => 6, - 'commandKey' => 'command', - ]), - $response->uri, - ); - - $this->assertEquals('some_value', $response->postedData->data->attr); -}); - -it('allows to test callSharpInstanceCommandFromList with a wizard step', function () { - $response = fakeResponse() - ->callSharpInstanceCommandFromList('leaves', 6, 'command', ['attr' => 'some_value'], 'my-step:123'); - - $this->assertEquals('my-step:123', $response->postedData->command_step); -}); - -it('allows to define a current breadcrumb', function () { - $response = fakeResponse() - ->withSharpBreadcrumb( - fn ($builder) => $builder - ->appendEntityList('trees') - ->appendShowPage('trees', 2) - ->appendShowPage('leaves', 6), - ) - ->getSharpForm('leaves', 6); - - $this->assertEquals( - 'http://localhost/sharp/root/s-list/trees/s-show/trees/2/s-show/leaves/6/s-form/leaves/6', - $response->uri, - ); -}); - -it('allows to test getSharpForm for edit with a custom breadcrumb with legacy API', function () { - $response = fakeResponse() - ->withSharpCurrentBreadcrumb( - ['list', 'leaves'], - ['show', 'leaves', 6], + $testMock->sharpList('person') + ->withFilter('job', 'physicist') + ->get() + ->assertOk(); +}); + +it('allows to test entity list instance command', function () { + /** @var \Mockery\MockInterface|SharpAssertionsTestCase $testMock */ + $testMock = Mockery::mock(SharpAssertionsTestCase::class)->makePartial(); + + $testMock->shouldReceive('postJson') + ->once() + ->with( + route('code16.sharp.api.list.command.instance', [ + 'entityKey' => 'person', + 'instanceId' => 1, + 'commandKey' => 'test', + ]), + [ + 'data' => ['foo' => 'bar'], + 'query' => ['filter_job' => 'physicist'], + 'command_step' => null, + ] ) - ->getSharpForm('leaves', 6); + ->andReturn(new TestResponse(new Response())); - $this->assertEquals( - route('code16.sharp.form.edit', ['s-list/leaves/s-show/leaves/6', 'leaves', 6]), - $response->uri, - ); -}); - -it('allows to define a current breadcrumb with legacy API', function () { - $response = fakeResponse() - ->withSharpCurrentBreadcrumb( - ['list', 'trees'], - ['show', 'trees', 2], - ['show', 'leaves', 6], + $testMock->sharpList('person') + ->withFilter('job', 'physicist') + ->callInstanceCommand(1, 'test', ['foo' => 'bar']) + ->assertOk(); +}); + +it('allows to test entity list entity command', function () { + /** @var \Mockery\MockInterface|SharpAssertionsTestCase $testMock */ + $testMock = Mockery::mock(SharpAssertionsTestCase::class)->makePartial(); + + $testMock->shouldReceive('postJson') + ->once() + ->with( + route('code16.sharp.api.list.command.entity', [ + 'entityKey' => 'person', + 'commandKey' => 'test', + ]), + [ + 'data' => ['foo' => 'bar'], + 'query' => ['filter_job' => 'physicist'], + 'command_step' => null, + ] ) - ->getSharpForm('leaves', 6); - - $this->assertEquals( - 'http://localhost/sharp/root/s-list/trees/s-show/trees/2/s-show/leaves/6/s-form/leaves/6', - $response->uri, - ); -}); - -it('allows to test getSharpForm for edit with global filter keys', function () { - fakeGlobalFilter('test-1'); + ->andReturn(new TestResponse(new Response())); - $this->assertEquals( - route('code16.sharp.form.edit', [ - 'globalFilter' => 'root', - 'parentUri' => 's-list/leaves', - 'entityKey' => 'leaves', - 'instanceId' => 6, - ]), - fakeResponse() - ->getSharpForm('leaves', 6) - ->uri, - ); - - $this->assertEquals( - route('code16.sharp.form.edit', [ - 'globalFilter' => 'one', - 'parentUri' => 's-list/leaves', - 'entityKey' => 'leaves', - 'instanceId' => 6, - ]), - fakeResponse() - ->withSharpGlobalFilterValues('one') - ->getSharpForm('leaves', 6) - ->uri, - ); - - fakeGlobalFilter('test-2'); - - $this->assertEquals( - route('code16.sharp.form.edit', [ - 'globalFilter' => 'one~two', - 'parentUri' => 's-list/leaves', - 'entityKey' => 'leaves', - 'instanceId' => 6, - ]), - fakeResponse() - ->withSharpGlobalFilterValues(['one', 'two']) - ->getSharpForm('leaves', 6) - ->uri, - ); + $testMock->sharpList('person') + ->withFilter('job', 'physicist') + ->callEntityCommand('test', ['foo' => 'bar']) + ->assertOk(); }); - -function fakeResponse() -{ - return new class('fake') extends Orchestra\Testbench\TestCase - { - use SharpAssertions; - use Tappable; - - public string $uri; - public mixed $postedData; - - public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null) - { - $this->uri = $uri; - - if ($parameters) { - $this->postedData = $parameters; - } elseif ($content) { - $this->postedData = json_decode($content); - } else { - $this->postedData = null; - } - - return new class($this->uri, $this->postedData) extends \Illuminate\Testing\TestResponse - { - public function __construct(public $uri, public $postedData) - { - parent::__construct(new \Illuminate\Http\Response()); - } - }; - } - }; -} diff --git a/tests/Unit/Utils/Testing/SharpAssertionsTestCase.php b/tests/Unit/Utils/Testing/SharpAssertionsTestCase.php new file mode 100644 index 000000000..3e392c2c4 --- /dev/null +++ b/tests/Unit/Utils/Testing/SharpAssertionsTestCase.php @@ -0,0 +1,11 @@ +getSharpShow('leaves', 6); + + $this->assertEquals( + route('code16.sharp.show.show', ['s-list/leaves', 'leaves', 6]), + $response->uri, + ); +}); + +it('allows to test getSharpForm for edit', function () { + $response = fakeResponse()->getSharpForm('leaves', 6); + + $this->assertEquals( + route('code16.sharp.form.edit', ['s-list/leaves', 'leaves', 6]), + $response->uri, + ); +}); + +it('allows to test getSharpForm for edit with a custom breadcrumb', function () { + $response = fakeResponse() + ->withSharpBreadcrumb( + fn ($builder) => $builder + ->appendEntityList('leaves') + ->appendShowPage('leaves', 6), + ) + ->getSharpForm('leaves', 6); + + $this->assertEquals( + route('code16.sharp.form.edit', ['s-list/leaves/s-show/leaves/6', 'leaves', 6]), + $response->uri, + ); +}); + +it('allows to test getSharpForm for single edit', function () { + $response = fakeResponse()->getSharpSingleForm('leaves'); + + $this->assertEquals( + route('code16.sharp.form.edit', ['s-list/leaves', 'leaves']), + $response->uri, + ); +}); + +it('allows to test getSharpForm for create', function () { + $response = fakeResponse()->getSharpForm('leaves'); + + $this->assertEquals( + route('code16.sharp.form.create', ['s-list/leaves', 'leaves']), + $response->uri, + ); +}); + +it('allows to test updateSharpForm for update', function () { + $response = fakeResponse()->updateSharpForm('leaves', 6, ['attr' => 'some_value']); + + $this->assertEquals( + route('code16.sharp.form.update', ['s-list/leaves', 'leaves', 6]), + $response->uri, + ); + + $this->assertEquals( + ['attr' => 'some_value'], + $response->postedData, + ); +}); + +it('allows to test updateSharpForm for single update', function () { + $response = fakeResponse() + ->updateSharpSingleForm('leaves', ['attr' => 'some_value']); + + $this->assertEquals( + route('code16.sharp.form.update', ['s-list/leaves', 'leaves']), + $response->uri, + ); + + $this->assertEquals( + ['attr' => 'some_value'], + $response->postedData, + ); +}); + +it('allows to test updateSharpForm for store', function () { + $response = fakeResponse()->storeSharpForm('leaves', ['attr' => 'some_value']); + + $this->assertEquals( + route('code16.sharp.form.store', ['s-list/leaves', 'leaves']), + $response->uri, + ); + + $this->assertEquals( + ['attr' => 'some_value'], + $response->postedData, + ); +}); + +it('allows to test deleteFromSharpList', function () { + $response = fakeResponse()->deleteFromSharpList('leaves', 6); + + $this->assertEquals( + route('code16.sharp.api.list.delete', ['leaves', 6]), + $response->uri, + ); +}); + +it('allows to test deleteSharpShow', function () { + $response = fakeResponse()->deleteFromSharpShow('leaves', 6); + + $this->assertEquals( + route('code16.sharp.show.delete', ['s-list/leaves', 'leaves', 6]), + $response->uri, + ); +}); + +it('allows to test callSharpInstanceCommandFromList', function () { + $response = fakeResponse() + ->callSharpInstanceCommandFromList('leaves', 6, 'command', ['attr' => 'some_value']); + + $this->assertEquals( + route('code16.sharp.api.list.command.instance', [ + 'entityKey' => 'leaves', + 'instanceId' => 6, + 'commandKey' => 'command', + ]), + $response->uri, + ); + + $this->assertEquals('some_value', $response->postedData->data->attr); +}); + +it('allows to test callSharpInstanceCommandFromShow', function () { + $response = fakeResponse() + ->callSharpInstanceCommandFromShow('leaves', 6, 'command', ['attr' => 'some_value']); + + $this->assertEquals( + route('code16.sharp.api.show.command.instance', [ + 'entityKey' => 'leaves', + 'instanceId' => 6, + 'commandKey' => 'command', + ]), + $response->uri, + ); + + $this->assertEquals('some_value', $response->postedData->data->attr); +}); + +it('allows to test callSharpInstanceCommandFromList with a wizard step', function () { + $response = fakeResponse() + ->callSharpInstanceCommandFromList('leaves', 6, 'command', ['attr' => 'some_value'], 'my-step:123'); + + $this->assertEquals('my-step:123', $response->postedData->command_step); +}); + +it('allows to define a current breadcrumb', function () { + $response = fakeResponse() + ->withSharpBreadcrumb( + fn ($builder) => $builder + ->appendEntityList('trees') + ->appendShowPage('trees', 2) + ->appendShowPage('leaves', 6), + ) + ->getSharpForm('leaves', 6); + + $this->assertEquals( + 'http://localhost/sharp/root/s-list/trees/s-show/trees/2/s-show/leaves/6/s-form/leaves/6', + $response->uri, + ); +}); + +it('allows to test getSharpForm for edit with a custom breadcrumb with legacy API', function () { + $response = fakeResponse() + ->withSharpCurrentBreadcrumb( + ['list', 'leaves'], + ['show', 'leaves', 6], + ) + ->getSharpForm('leaves', 6); + + $this->assertEquals( + route('code16.sharp.form.edit', ['s-list/leaves/s-show/leaves/6', 'leaves', 6]), + $response->uri, + ); +}); + +it('allows to define a current breadcrumb with legacy API', function () { + $response = fakeResponse() + ->withSharpCurrentBreadcrumb( + ['list', 'trees'], + ['show', 'trees', 2], + ['show', 'leaves', 6], + ) + ->getSharpForm('leaves', 6); + + $this->assertEquals( + 'http://localhost/sharp/root/s-list/trees/s-show/trees/2/s-show/leaves/6/s-form/leaves/6', + $response->uri, + ); +}); + +it('allows to test getSharpForm for edit with global filter keys', function () { + fakeGlobalFilter('test-1'); + + $this->assertEquals( + route('code16.sharp.form.edit', [ + 'globalFilter' => 'root', + 'parentUri' => 's-list/leaves', + 'entityKey' => 'leaves', + 'instanceId' => 6, + ]), + fakeResponse() + ->getSharpForm('leaves', 6) + ->uri, + ); + + $this->assertEquals( + route('code16.sharp.form.edit', [ + 'globalFilter' => 'one', + 'parentUri' => 's-list/leaves', + 'entityKey' => 'leaves', + 'instanceId' => 6, + ]), + fakeResponse() + ->withSharpGlobalFilterValues('one') + ->getSharpForm('leaves', 6) + ->uri, + ); + + fakeGlobalFilter('test-2'); + + $this->assertEquals( + route('code16.sharp.form.edit', [ + 'globalFilter' => 'one~two', + 'parentUri' => 's-list/leaves', + 'entityKey' => 'leaves', + 'instanceId' => 6, + ]), + fakeResponse() + ->withSharpGlobalFilterValues(['one', 'two']) + ->getSharpForm('leaves', 6) + ->uri, + ); +}); + +function fakeResponse() +{ + return new class('fake') extends Orchestra\Testbench\TestCase + { + use SharpAssertions; + + public $uri; + public $postedData; + + public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null) + { + $this->uri = $uri; + + if ($parameters) { + $this->postedData = $parameters; + } elseif ($content) { + $this->postedData = json_decode($content); + } else { + $this->postedData = null; + } + + return $this; + } + }; +} From 527678843af456428df3c1ebca4024e870a2e7ac Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 13 Jan 2026 16:49:49 +0100 Subject: [PATCH 3/4] wip --- ide.json | 11 ++ .../Testing/Commands/AssertableCommand.php | 125 ++++++++++++++++-- .../Testing/EntityList/PendingEntityList.php | 26 +++- src/Utils/Testing/Show/PendingShow.php | 66 +++++++++ tests/Http/SharpAssertionsHttpTest.php | 84 +++++++++++- 5 files changed, 290 insertions(+), 22 deletions(-) create mode 100644 src/Utils/Testing/Show/PendingShow.php diff --git a/ide.json b/ide.json index 06aefa593..d0bef3472 100644 --- a/ide.json +++ b/ide.json @@ -14,6 +14,17 @@ "parameters": [ 1 ] + }, + { + "classFqn": [ + "Code16\\Sharp\\Utils\\Testing\\Commands\\AssertableCommand" + ], + "methodNames": [ + "assertReturnsView" + ], + "parameters": [ + 1 + ] } ] }, diff --git a/src/Utils/Testing/Commands/AssertableCommand.php b/src/Utils/Testing/Commands/AssertableCommand.php index ab8570f30..4dc7873f0 100644 --- a/src/Utils/Testing/Commands/AssertableCommand.php +++ b/src/Utils/Testing/Commands/AssertableCommand.php @@ -2,31 +2,80 @@ namespace Code16\Sharp\Utils\Testing\Commands; +use Closure; +use Code16\Sharp\Dashboard\SharpDashboard; +use Code16\Sharp\EntityList\SharpEntityList; +use Code16\Sharp\Show\SharpShow; use Code16\Sharp\Utils\Testing\DelegatesToResponse; +use Illuminate\Support\Facades; use Illuminate\Support\Str; use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; +use Illuminate\View\View; use PHPUnit\Framework\Assert as PHPUnit; class AssertableCommand { use DelegatesToResponse; + public ?View $createdView = null; + public function __construct( - protected TestResponse $response, - ) {} + /** @var Closure(array,string): TestResponse */ + protected Closure $postCommand, + protected SharpEntityList|SharpShow|SharpDashboard $commandContainer, + protected array $data = [], + protected ?string $step = null, + ) { + $this->response = $this->post(); + } - public function assertReturnsHtml(): static + public function assertViewHas(mixed $key, mixed $value = null): static { - $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'html') + $this->response->original = $this->createdView; + $this->response->assertViewHas($key, $value); + + return $this; + } + + public function assertViewHasAll(mixed $bindings): static + { + $this->response->original = $this->createdView; + $this->response->assertViewHas($bindings); + + return $this; + } + + public function assertViewIs($value) + { + $this->response->original = $this->createdView; + $this->response->assertViewIs($value); + + return $this; + } + + public function assertReturnsView(?string $view = null, ?array $data = null): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'view') + ->etc() ); + if ($view) { + $this->assertViewIs($view); + } + + if ($data) { + $this->assertViewHasAll($data); + } + return $this; } public function assertReturnsInfo(string $message = ''): static { - $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'info') + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'info') ->when($message)->where('message', $message) ->etc() ); @@ -36,8 +85,10 @@ public function assertReturnsInfo(string $message = ''): static public function assertReturnsLink(string $url = ''): static { - $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'link') - ->when($url)->where('url', $url) + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'link') + ->when($url)->where('link', $url) + ->etc() ); return $this; @@ -45,27 +96,75 @@ public function assertReturnsLink(string $url = ''): static public function assertReturnsReload(): static { - $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'reload') + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'reload') + ->etc() ); return $this; } - public function assertReturnsRefresh(): static + public function assertReturnsRefresh(array $ids): static { - $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'refresh') + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'refresh') + ->etc() + ); + + PHPUnit::assertEqualsCanonicalizing( + $ids, + collect($this->response->json('items'))->pluck($this->commandContainer->getInstanceIdAttribute())->all() ); return $this; } - public function assertReturnsStep(string $step): static + public function assertReturnsStep(?string $step = null): static { - $this->response->assertJson(fn (AssertableJson $json) => $json->where('action', 'step') + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'step') + ->etc() ); - PHPUnit::assertEquals($step, Str::before($this->response->json('step'), ':')); + if ($step) { + PHPUnit::assertEquals($step, Str::before($this->response->json('step'), ':')); + } + + return $this; + } + + public function assertReturnsDownload(?string $filename = null): static + { + $this->response->assertStreamed(); + + if ($filename) { + preg_match('/filename="?([^";]+)"?/', $this->response->headers->get('Content-Disposition'), $matches); + PHPUnit::assertEquals($filename, $matches[1] ?? null); + } return $this; } + + public function callNextStep(array $data = []): static + { + $this->assertReturnsStep(); + + return new AssertableCommand( + $this->postCommand, + commandContainer: $this->commandContainer, + data: $data, + step: $this->response->json('step'), + ); + } + + protected function post(): TestResponse + { + Facades\View::creator('*', function (View $view) { + $this->createdView = $view; + }); + + return tap(($this->postCommand)($this->data, $this->step), function () { + Facades\Event::forget('creating: *'); + }); + } } diff --git a/src/Utils/Testing/EntityList/PendingEntityList.php b/src/Utils/Testing/EntityList/PendingEntityList.php index f17039418..cccdf0105 100644 --- a/src/Utils/Testing/EntityList/PendingEntityList.php +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -7,6 +7,7 @@ use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Testing\Commands\AssertableCommand; use Code16\Sharp\Utils\Testing\GeneratesSharpUrl; +use Code16\Sharp\Utils\Testing\Show\PendingShow; use Illuminate\Foundation\Testing\TestCase; class PendingEntityList @@ -14,8 +15,8 @@ class PendingEntityList use GeneratesSharpUrl; protected SharpEntityList $entityList; - protected array $filterValues = []; protected string $entityKey; + protected array $filterValues = []; public function __construct( /** @var TestCase $test */ @@ -26,6 +27,11 @@ public function __construct( $this->entityList = app(SharpEntityManager::class)->entityFor($this->entityKey)->getListOrFail(); } + public function sharpShow(string $entityKey, string|int $instanceId): PendingShow + { + return new PendingShow($this->test, $entityKey, $instanceId, parent: $this); + } + public function withFilter(string $filterKey, mixed $value): static { $key = $this->entityList->filterContainer()->findFilterHandler($filterKey)->getKey(); @@ -60,7 +66,7 @@ public function callEntityCommand( : $commandKeyOrClassName; return new AssertableCommand( - $this + fn ($data, $step) => $this ->test ->withHeader( SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, @@ -76,9 +82,12 @@ public function callEntityCommand( [ 'data' => $data, 'query' => $this->entityListQueryParams(), - 'command_step' => $commandStep, + 'command_step' => $step, ], - ) + ), + commandContainer: $this->entityList, + data: $data, + step: $commandStep ); } @@ -95,7 +104,7 @@ public function callInstanceCommand( : $commandKeyOrClassName; return new AssertableCommand( - $this + fn ($data, $step) => $this ->test ->withHeader( SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, @@ -111,9 +120,12 @@ public function callInstanceCommand( [ 'data' => $data, 'query' => $this->entityListQueryParams(), - 'command_step' => $commandStep, + 'command_step' => $step, ], - ) + ), + commandContainer: $this->entityList, + data: $data, + step: $commandStep, ); } diff --git a/src/Utils/Testing/Show/PendingShow.php b/src/Utils/Testing/Show/PendingShow.php new file mode 100644 index 000000000..dbc2ded0d --- /dev/null +++ b/src/Utils/Testing/Show/PendingShow.php @@ -0,0 +1,66 @@ +entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); + $this->entityList = app(SharpEntityManager::class)->entityFor($this->entityKey)->getShowOrFail(); + } + + public function callInstanceCommand( + string $commandKeyOrClassName, + array $data = [],"" + ?string $commandStep = null, + ): AssertableCommand { + $this->setGlobalFilterUrlDefault(); + + $commandKey = class_exists($commandKeyOrClassName) + ? class_basename($commandKeyOrClassName) + : $commandKeyOrClassName; + + return new AssertableCommand( + fn ($data, $step) => $this + ->test + ->withHeader( + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, + $this->buildCurrentPageUrl( + $this->breadcrumbBuilder($this->entityKey, $this->instanceId) + ), + ) + ->postJson( + route( + 'code16.sharp.api.show.command.instance', + ['entityKey' => $this->entityKey, 'instanceId' => $this->instanceId, 'commandKey' => $commandKey] + ), + [ + 'data' => $data, + 'command_step' => $step, + ], + ), + commandContainer: $this->entityList, + data: $data, + step: $commandStep, + ); + } +} diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index ca9557374..3510daf98 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -1,11 +1,13 @@ use(SharpAssertions::class); @@ -14,7 +16,7 @@ sharp()->config()->declareEntity(PersonEntity::class); }); -it('gets an entity list', function () { +it('get & assert an entity list', function () { fakeListFor(PersonEntity::class, new class() extends PersonList { public function getListData(): array @@ -27,12 +29,13 @@ public function getListData(): array $this->sharpList(PersonEntity::class) ->get() + // ->getListData() ->assertOk() ->assertListCount(1) ->assertListContains(['name' => 'Marie Curie']); }); -it('call an entity list entity command', function () { +it('call & assert an entity list entity command', function () { fakeListFor('person', new class() extends PersonList { protected function getEntityCommands(): ?array @@ -52,9 +55,20 @@ public function buildFormFields(FieldsContainer $formFields): void public function execute(array $data = []): array { + if ($data['action'] === 'download') { + Storage::fake('files'); + UploadedFile::fake() + ->create('account.pdf', 100, 'application/pdf') + ->storeAs('pdf', 'account.pdf', ['disk' => 'files']); + } + return match ($data['action']) { 'info' => $this->info('ok'), 'link' => $this->link('https://example.org'), + 'view' => $this->view('fixtures::test', ['text' => 'text']), + 'reload' => $this->reload(), + 'download' => $this->download('pdf/account.pdf', 'account.pdf', 'files'), + 'refresh' => $this->refresh([1, 2]), }; } }, @@ -69,4 +83,70 @@ public function execute(array $data = []): array $this->sharpList(PersonEntity::class) ->callEntityCommand('cmd', ['action' => 'link']) ->assertReturnsLink('https://example.org'); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'view']) + ->assertReturnsView('fixtures::test', [ + 'text' => 'text', + ]); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'reload']) + ->assertReturnsReload(); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'download']) + ->assertReturnsDownload('account.pdf'); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'refresh']) + ->assertReturnsRefresh([1, 2]); +}); + +it('call & assert an entity list entity wiard command', function () { + fakeListFor('person', new class() extends PersonList + { + protected function getEntityCommands(): ?array + { + return [ + 'wizard' => new class() extends EntityWizardCommand + { + public function label(): ?string + { + return 'my command'; + } + + public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('name')); + } + + protected function executeFirstStep(array $data): array + { + $this->validate($data, ['name' => 'required']); + + return $this->toStep('second-step'); + } + + public function buildFormFieldsForStepSecondStep(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('age')); + } + + protected function executeStepSecondStep(array $data): array + { + expect($data)->toEqual(['age' => 30]); + + return $this->reload(); + } + }, + ]; + } + }); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('wizard', ['name' => 'John']) + ->assertReturnsStep('second-step') + ->callNextStep(['age' => 30]) + ->assertReturnsReload(); }); From 7b6522dc05000011858d2eab1507821840cd1cbe Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 13 Jan 2026 19:42:27 +0100 Subject: [PATCH 4/4] wip --- src/Http/Controllers/ShowController.php | 4 ++ src/Http/Controllers/SingleShowController.php | 4 ++ .../Testing/EntityList/PendingEntityList.php | 24 ++++--- src/Utils/Testing/Form/PendingForm.php | 44 ++++++++++++ src/Utils/Testing/GeneratesCurrentPageUrl.php | 20 ++++++ .../Testing/GeneratesGlobalFilterUrl.php | 24 +++++++ src/Utils/Testing/GeneratesSharpUrl.php | 64 ----------------- src/Utils/Testing/IsPendingComponent.php | 70 +++++++++++++++++++ src/Utils/Testing/SharpAssertions.php | 40 ++++++++++- src/Utils/Testing/Show/AssertableShow.php | 31 ++++++++ src/Utils/Testing/Show/PendingShow.php | 56 +++++++++++---- tests/Http/SharpAssertionsHttpTest.php | 38 ++++++++++ 12 files changed, 331 insertions(+), 88 deletions(-) create mode 100644 src/Utils/Testing/Form/PendingForm.php create mode 100644 src/Utils/Testing/GeneratesCurrentPageUrl.php create mode 100644 src/Utils/Testing/GeneratesGlobalFilterUrl.php delete mode 100644 src/Utils/Testing/GeneratesSharpUrl.php create mode 100644 src/Utils/Testing/IsPendingComponent.php create mode 100644 src/Utils/Testing/Show/AssertableShow.php diff --git a/src/Http/Controllers/ShowController.php b/src/Http/Controllers/ShowController.php index e71d9adb1..cb4c595b2 100644 --- a/src/Http/Controllers/ShowController.php +++ b/src/Http/Controllers/ShowController.php @@ -57,6 +57,10 @@ public function show(string $globalFilter, string $parentUri, EntityKey $entityK $this->addPreloadHeadersForShowEntityLists($payload); + if (app()->environment('testing')) { + Inertia::share('_rawData', $showData); + } + return Inertia::render('Show/Show', [ 'show' => $payload, 'breadcrumb' => BreadcrumbData::from([ diff --git a/src/Http/Controllers/SingleShowController.php b/src/Http/Controllers/SingleShowController.php index 925aad556..9583a80d0 100644 --- a/src/Http/Controllers/SingleShowController.php +++ b/src/Http/Controllers/SingleShowController.php @@ -53,6 +53,10 @@ public function show(string $globalFilter, EntityKey $entityKey) $this->addPreloadHeadersForShowEntityLists($payload); + if (app()->environment('testing')) { + Inertia::share('_rawData', $showData); + } + return Inertia::render('Show/Show', [ 'show' => $payload, 'breadcrumb' => BreadcrumbData::from([ diff --git a/src/Utils/Testing/EntityList/PendingEntityList.php b/src/Utils/Testing/EntityList/PendingEntityList.php index cccdf0105..5e54bef5c 100644 --- a/src/Utils/Testing/EntityList/PendingEntityList.php +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -6,22 +6,23 @@ use Code16\Sharp\Http\Context\SharpBreadcrumb; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Testing\Commands\AssertableCommand; -use Code16\Sharp\Utils\Testing\GeneratesSharpUrl; +use Code16\Sharp\Utils\Testing\IsPendingComponent; use Code16\Sharp\Utils\Testing\Show\PendingShow; use Illuminate\Foundation\Testing\TestCase; class PendingEntityList { - use GeneratesSharpUrl; + use IsPendingComponent; protected SharpEntityList $entityList; - protected string $entityKey; + public string $entityKey; protected array $filterValues = []; public function __construct( /** @var TestCase $test */ protected object $test, - string $entityKey + string $entityKey, + public ?PendingShow $parent = null, ) { $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); $this->entityList = app(SharpEntityManager::class)->entityFor($this->entityKey)->getListOrFail(); @@ -29,7 +30,12 @@ public function __construct( public function sharpShow(string $entityKey, string|int $instanceId): PendingShow { - return new PendingShow($this->test, $entityKey, $instanceId, parent: $this); + return new PendingShow( + $this->test, + $entityKey, + $instanceId, + parent: $this->parent instanceof PendingShow ? $this->parent : $this + ); } public function withFilter(string $filterKey, mixed $value): static @@ -70,9 +76,7 @@ public function callEntityCommand( ->test ->withHeader( SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->buildCurrentPageUrl( - $this->breadcrumbBuilder($this->entityKey) - ), + $this->getCurrentPageUrlFromParents(), ) ->postJson( route( @@ -108,9 +112,7 @@ public function callInstanceCommand( ->test ->withHeader( SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->buildCurrentPageUrl( - $this->breadcrumbBuilder($this->entityKey, $instanceId) - ), + $this->getCurrentPageUrlFromParents(), ) ->postJson( route( diff --git a/src/Utils/Testing/Form/PendingForm.php b/src/Utils/Testing/Form/PendingForm.php new file mode 100644 index 000000000..940e98acb --- /dev/null +++ b/src/Utils/Testing/Form/PendingForm.php @@ -0,0 +1,44 @@ +entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); + $this->show = app(SharpEntityManager::class)->entityFor($this->entityKey)->getShowOrFail(); + } + + public function get(): TestResponse + { + $this->setGlobalFilterUrlDefault(); + + return $this->test + ->get(route('code16.sharp.form.edit', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + 'instanceId' => $this->instanceId, + ])); + } +} diff --git a/src/Utils/Testing/GeneratesCurrentPageUrl.php b/src/Utils/Testing/GeneratesCurrentPageUrl.php new file mode 100644 index 000000000..6bb9c58d0 --- /dev/null +++ b/src/Utils/Testing/GeneratesCurrentPageUrl.php @@ -0,0 +1,20 @@ +config()->get('custom_url_segment'), + sharp()->context()->globalFilterUrlSegmentValue(), + $builder->generateUri() + ) + ); + } +} diff --git a/src/Utils/Testing/GeneratesGlobalFilterUrl.php b/src/Utils/Testing/GeneratesGlobalFilterUrl.php new file mode 100644 index 000000000..14b910eab --- /dev/null +++ b/src/Utils/Testing/GeneratesGlobalFilterUrl.php @@ -0,0 +1,24 @@ +globalFilter = collect((array) $globalFilterValues) + ->implode(GlobalFilters::$valuesUrlSeparator); + + return $this; + } + + private function setGlobalFilterUrlDefault(): void + { + URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + } +} diff --git a/src/Utils/Testing/GeneratesSharpUrl.php b/src/Utils/Testing/GeneratesSharpUrl.php deleted file mode 100644 index 6fb632bdf..000000000 --- a/src/Utils/Testing/GeneratesSharpUrl.php +++ /dev/null @@ -1,64 +0,0 @@ -breadcrumbBuilder = $callback(new BreadcrumbBuilder()); - - return $this; - } - - public function withSharpGlobalFilterValues(array|string $globalFilterValues): self - { - $this->globalFilter = collect((array) $globalFilterValues) - ->implode(GlobalFilters::$valuesUrlSeparator); - - return $this; - } - - private function setGlobalFilterUrlDefault(): void - { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); - } - - private function breadcrumbBuilder(string $entityKey, ?string $instanceId = null): BreadcrumbBuilder - { - if (isset($this->breadcrumbBuilder)) { - return $this->breadcrumbBuilder; - } - - return (new BreadcrumbBuilder()) - ->appendEntityList($entityKey) - ->when($instanceId, fn ($builder) => $builder->appendShowPage($entityKey, $instanceId)); - } - - private function buildCurrentPageUrl(BreadcrumbBuilder $builder): string - { - return url( - sprintf( - '/%s/%s/%s', - sharp()->config()->get('custom_url_segment'), - sharp()->context()->globalFilterUrlSegmentValue(), - $builder->generateUri() - ) - ); - } -} diff --git a/src/Utils/Testing/IsPendingComponent.php b/src/Utils/Testing/IsPendingComponent.php new file mode 100644 index 000000000..80bf6ff7b --- /dev/null +++ b/src/Utils/Testing/IsPendingComponent.php @@ -0,0 +1,70 @@ +breadcrumbBuilder($this->parents())->generateUri(); + } + + protected function getCurrentPageUrlFromParents(): string + { + return $this->buildCurrentPageUrl($this->breadcrumbBuilder([...$this->parents(), $this])); + } + + protected function breadcrumbBuilder(array $components): BreadcrumbBuilder + { + $breadcrumb = new BreadcrumbBuilder(); + $first = $components[0] ?? $this; + + // fill the breadcrumb if needed + if ($first instanceof PendingShow && $first->instanceId) { + $breadcrumb->appendEntityList($first->entityKey); + } elseif ($first instanceof PendingForm) { + if ($first->instanceId) { + $breadcrumb->appendEntityList($first->entityKey); + if (app(SharpEntityManager::class)->entityFor($first->entityKey)->hasShow()) { + $breadcrumb->appendShowPage($first->entityKey, $first->instanceId); + } + } else { + $breadcrumb->appendSingleShowPage($first->entityKey); + } + } + + foreach ($components as $component) { + if ($component instanceof PendingEntityList) { + $breadcrumb->appendEntityList($component->entityKey); + } elseif ($component instanceof PendingShow) { + if ($component->instanceId) { + $breadcrumb->appendShowPage($component->entityKey, $component->instanceId); + } else { + $breadcrumb->appendSingleShowPage($component->entityKey); + } + } + } + + return $breadcrumb; + } + + protected function parents(): array + { + $parents = []; + + for ($parent = $this->parent; $parent; $parent = $parent->parent) { + $parents[] = $parent; + } + + return array_reverse($parents); + } +} diff --git a/src/Utils/Testing/SharpAssertions.php b/src/Utils/Testing/SharpAssertions.php index a22318763..d97175721 100644 --- a/src/Utils/Testing/SharpAssertions.php +++ b/src/Utils/Testing/SharpAssertions.php @@ -2,20 +2,36 @@ namespace Code16\Sharp\Utils\Testing; +use Closure; use Code16\Sharp\Http\Context\SharpBreadcrumb; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Links\BreadcrumbBuilder; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; +use Code16\Sharp\Utils\Testing\Form\PendingForm; +use Code16\Sharp\Utils\Testing\Show\PendingShow; trait SharpAssertions { - use GeneratesSharpUrl; + use GeneratesCurrentPageUrl; + use GeneratesGlobalFilterUrl; + + private BreadcrumbBuilder $breadcrumbBuilder; public function sharpList(string $entityClassNameOrKey): PendingEntityList { return new PendingEntityList($this, $entityClassNameOrKey); } + public function sharpShow(string $entityClassNameOrKey, int|string|null $instanceId = null): PendingShow + { + return new PendingShow($this, $entityClassNameOrKey, $instanceId); + } + + public function sharpForm(string $entityClassNameOrKey, int|string|null $instanceId = null): PendingForm + { + return new PendingForm($this, $entityClassNameOrKey, $instanceId); + } + /** * @deprecated use withSharpBreadcrumb() instead */ @@ -34,6 +50,17 @@ public function withSharpCurrentBreadcrumb(...$breadcrumb): self return $this; } + /** + * @param (\Closure(BreadcrumbBuilder): BreadcrumbBuilder) $callback + * @return $this + */ + public function withSharpBreadcrumb(Closure $callback): self + { + $this->breadcrumbBuilder = $callback(new BreadcrumbBuilder()); + + return $this; + } + public function deleteFromSharpShow(string $entityClassNameOrKey, mixed $instanceId) { $this->setGlobalFilterUrlDefault(); @@ -294,6 +321,17 @@ public function loginAsSharpUser($user): self return $this->actingAs($user, sharp()->config()->get('auth.guard') ?: config('auth.defaults.guard')); } + private function breadcrumbBuilder(string $entityKey, ?string $instanceId = null): BreadcrumbBuilder + { + if (isset($this->breadcrumbBuilder)) { + return $this->breadcrumbBuilder; + } + + return (new BreadcrumbBuilder()) + ->appendEntityList($entityKey) + ->when($instanceId, fn ($builder) => $builder->appendShowPage($entityKey, $instanceId)); + } + private function resolveEntityKey(string $entityClassNameOrKey): string { return app(SharpEntityManager::class)->entityKeyFor($entityClassNameOrKey); diff --git a/src/Utils/Testing/Show/AssertableShow.php b/src/Utils/Testing/Show/AssertableShow.php new file mode 100644 index 000000000..19ea8b900 --- /dev/null +++ b/src/Utils/Testing/Show/AssertableShow.php @@ -0,0 +1,31 @@ +response->inertiaProps('_rawData'); + } + + public function assertShowData(array $expectedData): self + { + $this->response->assertInertia(fn (AssertableInertia $page) => $page + ->has('_rawData', fn (AssertableJson $json) => $json->whereAll($expectedData)->etc()) + ); + + return $this; + } +} diff --git a/src/Utils/Testing/Show/PendingShow.php b/src/Utils/Testing/Show/PendingShow.php index dbc2ded0d..596547a50 100644 --- a/src/Utils/Testing/Show/PendingShow.php +++ b/src/Utils/Testing/Show/PendingShow.php @@ -2,35 +2,69 @@ namespace Code16\Sharp\Utils\Testing\Show; -use Code16\Sharp\EntityList\SharpEntityList; use Code16\Sharp\Http\Context\SharpBreadcrumb; +use Code16\Sharp\Show\SharpShow; +use Code16\Sharp\Show\SharpSingleShow; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Testing\Commands\AssertableCommand; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; -use Code16\Sharp\Utils\Testing\GeneratesSharpUrl; +use Code16\Sharp\Utils\Testing\Form\PendingForm; +use Code16\Sharp\Utils\Testing\GeneratesGlobalFilterUrl; +use Code16\Sharp\Utils\Testing\IsPendingComponent; use Illuminate\Foundation\Testing\TestCase; class PendingShow { - use GeneratesSharpUrl; + use GeneratesGlobalFilterUrl; + use IsPendingComponent; - protected SharpEntityList $entityList; - protected string $entityKey; + protected SharpShow $show; + public string $entityKey; public function __construct( /** @var TestCase $test */ protected object $test, string $entityKey, protected string|int|null $instanceId = null, - protected PendingEntityList|PendingShow|null $parent = null, + public PendingEntityList|PendingShow|null $parent = null, ) { $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); - $this->entityList = app(SharpEntityManager::class)->entityFor($this->entityKey)->getShowOrFail(); + $this->show = app(SharpEntityManager::class)->entityFor($this->entityKey)->getShowOrFail(); + } + + public function sharpForm(string $entityClassNameOrKey): PendingForm + { + return new PendingForm($this->test, $entityClassNameOrKey, $this->instanceId, parent: $this); + } + + public function sharpListField(string $entityClassNameOrKey, ?string $entityListKey = null): PendingEntityList + { + return new PendingEntityList($this->test, $entityClassNameOrKey, parent: $this); + } + + public function get(): AssertableShow + { + $this->setGlobalFilterUrlDefault(); + + return new AssertableShow( + $this->test + ->get($this->show instanceof SharpSingleShow + ? route('code16.sharp.single-show', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + ]) + : route('code16.sharp.show.show', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + 'instanceId' => $this->instanceId, + ]) + ) + ); } public function callInstanceCommand( string $commandKeyOrClassName, - array $data = [],"" + array $data = [], ?string $commandStep = null, ): AssertableCommand { $this->setGlobalFilterUrlDefault(); @@ -44,9 +78,7 @@ public function callInstanceCommand( ->test ->withHeader( SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->buildCurrentPageUrl( - $this->breadcrumbBuilder($this->entityKey, $this->instanceId) - ), + $this->getCurrentPageUrlFromParents(), ) ->postJson( route( @@ -58,7 +90,7 @@ public function callInstanceCommand( 'command_step' => $step, ], ), - commandContainer: $this->entityList, + commandContainer: $this->show, data: $data, step: $commandStep, ); diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index 3510daf98..e2a6c7e6b 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -5,6 +5,7 @@ use Code16\Sharp\Form\Fields\SharpFormTextField; use Code16\Sharp\Tests\Fixtures\Entities\PersonEntity; use Code16\Sharp\Tests\Fixtures\Sharp\PersonList; +use Code16\Sharp\Tests\Fixtures\Sharp\PersonShow; use Code16\Sharp\Utils\Fields\FieldsContainer; use Code16\Sharp\Utils\Testing\SharpAssertions; use Illuminate\Http\UploadedFile; @@ -150,3 +151,40 @@ protected function executeStepSecondStep(array $data): array ->callNextStep(['age' => 30]) ->assertReturnsReload(); }); + +test('get & assert show', function () { + fakeShowFor('person', new class() extends PersonShow + { + public function find($id): array + { + return ['name' => 'John Doe', 'age' => 31]; + } + }); + + $this->sharpShow(PersonEntity::class, 1) + ->get() + ->assertOk() + ->assertShowData(['name' => 'John Doe']); +}); + +test('get & assert show EEL', function () { + fakeShowFor('person', new class() extends PersonShow + { + public function find($id): array + { + return ['name' => 'John Doe', 'age' => 31]; + } + }); + + $this->sharpShow(PersonEntity::class, 1) + ->sharpListField(PersonEntity::class) + ->get() + ->assertOk() + ->assertShowData(['name' => 'John Doe']); +}); + +test('get & assert form', function () { + $this->sharpForm(PersonEntity::class, 1) + ->get() + ->assertOk(); +});