From 295827f1ab2020d867d0748b6298fc08f6a35fa2 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 9 Jan 2026 17:41:09 +0100 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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(); +}); From 4c50fe86058574906fa218ce29eb9e2252a06b85 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 14 Jan 2026 13:56:04 +0100 Subject: [PATCH 05/12] wip EEL --- src/Show/Fields/SharpShowEntityListField.php | 1 + .../EntityList/AssertableEntityList.php | 12 +++- .../Testing/EntityList/PendingEntityList.php | 59 +++++++++++++++---- src/Utils/Testing/Form/PendingForm.php | 2 +- src/Utils/Testing/IsPendingComponent.php | 2 +- src/Utils/Testing/Show/PendingShow.php | 6 +- tests/Http/SharpAssertionsHttpTest.php | 23 +++++++- 7 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/Show/Fields/SharpShowEntityListField.php b/src/Show/Fields/SharpShowEntityListField.php index 8b6fa64a1..a572f9926 100644 --- a/src/Show/Fields/SharpShowEntityListField.php +++ b/src/Show/Fields/SharpShowEntityListField.php @@ -157,6 +157,7 @@ public function toArray(): array ]), function (array &$options) { $options['endpointUrl'] = route('code16.sharp.api.list', [ + 'globalFilter' => sharp()->context()->globalFilterUrlSegmentValue(), 'entityKey' => $this->entityListKey, 'current_page_url' => request()->url(), ...app(SharpEntityManager::class) diff --git a/src/Utils/Testing/EntityList/AssertableEntityList.php b/src/Utils/Testing/EntityList/AssertableEntityList.php index e703cb02c..241d1cf29 100644 --- a/src/Utils/Testing/EntityList/AssertableEntityList.php +++ b/src/Utils/Testing/EntityList/AssertableEntityList.php @@ -3,6 +3,7 @@ namespace Code16\Sharp\Utils\Testing\EntityList; use Code16\Sharp\Utils\Testing\DelegatesToResponse; +use Code16\Sharp\Utils\Testing\Show\PendingShow; use Illuminate\Testing\TestResponse; use PHPUnit\Framework\Assert as PHPUnit; @@ -12,6 +13,7 @@ class AssertableEntityList public function __construct( protected TestResponse $response, + protected PendingEntityList $pendingEntityList, ) {} public function assertListCount(int $count): self @@ -24,8 +26,10 @@ public function assertListCount(int $count): self 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) - ), + 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) @@ -37,6 +41,8 @@ public function assertListContains(array $attributes): self protected function listData(): array { - return $this->response->inertiaProps('entityList.data'); + return $this->pendingEntityList->parent instanceof PendingShow + ? $this->response->json('data') + : $this->response->inertiaProps('entityList.data'); } } diff --git a/src/Utils/Testing/EntityList/PendingEntityList.php b/src/Utils/Testing/EntityList/PendingEntityList.php index 5e54bef5c..9259ee712 100644 --- a/src/Utils/Testing/EntityList/PendingEntityList.php +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -4,11 +4,15 @@ use Code16\Sharp\EntityList\SharpEntityList; use Code16\Sharp\Http\Context\SharpBreadcrumb; +use Code16\Sharp\Show\Fields\SharpShowEntityListField; +use Code16\Sharp\Show\Fields\SharpShowField; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Testing\Commands\AssertableCommand; +use Code16\Sharp\Utils\Testing\Form\PendingForm; use Code16\Sharp\Utils\Testing\IsPendingComponent; use Code16\Sharp\Utils\Testing\Show\PendingShow; use Illuminate\Foundation\Testing\TestCase; +use PHPUnit\Framework\Assert as PHPUnit; class PendingEntityList { @@ -26,16 +30,20 @@ public function __construct( ) { $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); $this->entityList = app(SharpEntityManager::class)->entityFor($this->entityKey)->getListOrFail(); + + if ($this->parent instanceof PendingShow) { + PHPUnit::assertNotNull($this->entityListShowField(), sprintf('Unknown entity list show field "%s"', $this->entityKey)); + } } - public function sharpShow(string $entityKey, string|int $instanceId): PendingShow + public function sharpShow(string $entityClassNameOrKey, string|int $instanceId): PendingShow { - return new PendingShow( - $this->test, - $entityKey, - $instanceId, - parent: $this->parent instanceof PendingShow ? $this->parent : $this - ); + return new PendingShow($this->test, $entityClassNameOrKey, $instanceId, parent: $this); + } + + public function sharpForm(string $entityClassNameOrKey, string|int $instanceId): PendingForm + { + return new PendingForm($this->test, $entityClassNameOrKey, $instanceId, parent: $this); } public function withFilter(string $filterKey, mixed $value): static @@ -51,12 +59,26 @@ public function get(): AssertableEntityList $this->setGlobalFilterUrlDefault(); return new AssertableEntityList( - $this->test->get( - route('code16.sharp.list', [ - 'entityKey' => $this->entityKey, - ...$this->entityListQueryParams(), - ]) - ) + $this->parent instanceof PendingShow + ? $this->test + ->withHeader( + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, + $this->getCurrentPageUrlFromParents(), + ) + ->get( + route('code16.sharp.api.list', [ + 'entityKey' => $this->entityKey, + ...$this->entityList + ->filterContainer() + ->getQueryParamsFromFilterValues($this->entityListShowField()->toArray()['hiddenFilters'] ?? []), + ...$this->entityListQueryParams(), + ]) + ) + : $this->test->get(route('code16.sharp.list', [ + 'entityKey' => $this->entityKey, + ...$this->entityListQueryParams(), + ])), + $this, ); } @@ -131,6 +153,17 @@ public function callInstanceCommand( ); } + protected function entityListShowField(): ?SharpShowEntityListField + { + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->parent instanceof PendingShow + ? $this->parent->show->getBuiltFields() + ->first(fn (SharpShowField $field) => $field instanceof SharpShowEntityListField + && $field->toArray()['entityListKey'] === $this->entityKey + ) + : null; + } + protected function entityListQueryParams(): array { return $this->entityList diff --git a/src/Utils/Testing/Form/PendingForm.php b/src/Utils/Testing/Form/PendingForm.php index 940e98acb..b3d1703c6 100644 --- a/src/Utils/Testing/Form/PendingForm.php +++ b/src/Utils/Testing/Form/PendingForm.php @@ -23,7 +23,7 @@ public function __construct( /** @var TestCase $test */ protected object $test, string $entityKey, - protected string|int|null $instanceId = null, + public string|int|null $instanceId = null, public PendingEntityList|PendingShow|null $parent = null, ) { $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); diff --git a/src/Utils/Testing/IsPendingComponent.php b/src/Utils/Testing/IsPendingComponent.php index 80bf6ff7b..383e3b5fd 100644 --- a/src/Utils/Testing/IsPendingComponent.php +++ b/src/Utils/Testing/IsPendingComponent.php @@ -43,7 +43,7 @@ protected function breadcrumbBuilder(array $components): BreadcrumbBuilder } foreach ($components as $component) { - if ($component instanceof PendingEntityList) { + if ($component instanceof PendingEntityList && ! $component->parent) { $breadcrumb->appendEntityList($component->entityKey); } elseif ($component instanceof PendingShow) { if ($component->instanceId) { diff --git a/src/Utils/Testing/Show/PendingShow.php b/src/Utils/Testing/Show/PendingShow.php index 596547a50..8b549b731 100644 --- a/src/Utils/Testing/Show/PendingShow.php +++ b/src/Utils/Testing/Show/PendingShow.php @@ -18,14 +18,14 @@ class PendingShow use GeneratesGlobalFilterUrl; use IsPendingComponent; - protected SharpShow $show; + public SharpShow $show; public string $entityKey; public function __construct( /** @var TestCase $test */ protected object $test, string $entityKey, - protected string|int|null $instanceId = null, + public string|int|null $instanceId = null, public PendingEntityList|PendingShow|null $parent = null, ) { $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); @@ -37,7 +37,7 @@ 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 + public function sharpListField(string $entityClassNameOrKey): PendingEntityList { return new PendingEntityList($this->test, $entityClassNameOrKey, parent: $this); } diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index e2a6c7e6b..d4ca8df80 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -3,6 +3,7 @@ use Code16\Sharp\EntityList\Commands\EntityCommand; use Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand; use Code16\Sharp\Form\Fields\SharpFormTextField; +use Code16\Sharp\Show\Fields\SharpShowEntityListField; use Code16\Sharp\Tests\Fixtures\Entities\PersonEntity; use Code16\Sharp\Tests\Fixtures\Sharp\PersonList; use Code16\Sharp\Tests\Fixtures\Sharp\PersonShow; @@ -30,7 +31,6 @@ public function getListData(): array $this->sharpList(PersonEntity::class) ->get() - // ->getListData() ->assertOk() ->assertListCount(1) ->assertListContains(['name' => 'Marie Curie']); @@ -170,17 +170,36 @@ public function find($id): array test('get & assert show EEL', function () { fakeShowFor('person', new class() extends PersonShow { + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField( + SharpShowEntityListField::make(PersonEntity::class) + ); + } + public function find($id): array { return ['name' => 'John Doe', 'age' => 31]; } }); + fakeListFor(PersonEntity::class, new class() extends PersonList + { + public function getListData(): array + { + return [ + ['id' => 1, 'name' => 'Marie Curie'], + ['id' => 2, 'name' => 'Albert Einstein'], + ]; + } + }); + $this->sharpShow(PersonEntity::class, 1) ->sharpListField(PersonEntity::class) ->get() ->assertOk() - ->assertShowData(['name' => 'John Doe']); + ->assertListCount(2) + ->assertListContains(['name' => 'Marie Curie']); }); test('get & assert form', function () { From 29dff6259632ace7729555abb474df0bbe6963dc Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 14 Jan 2026 15:49:46 +0100 Subject: [PATCH 06/12] Fix sharp context between requests --- src/SharpInternalServiceProvider.php | 6 +++++- src/Utils/SharpUtil.php | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/SharpInternalServiceProvider.php b/src/SharpInternalServiceProvider.php index 8eedf3a4b..89f90b056 100644 --- a/src/SharpInternalServiceProvider.php +++ b/src/SharpInternalServiceProvider.php @@ -132,6 +132,10 @@ public function register() }); $this->app->register(InertiaServiceProvider::class); + + $this->app->rebinding('request', function ($app, $request) { + $this->app->get(SharpUtil::class)->reset(); + }); } protected function declareMiddleware(): void @@ -244,7 +248,7 @@ private function resetSharp() $this->app->get(SharpMenuManager::class)->reset(); $this->app->get(SharpAuthorizationManager::class)->reset(); $this->app->get(SharpUploadManager::class)->reset(); - $this->app->get(SharpUtil::class)->__construct(); + $this->app->get(SharpUtil::class)->reset(); $this->app->get(SharpImageManager::class)->__construct(); $this->app->get(AddLinkHeadersForPreloadedRequests::class)->reset(); } diff --git a/src/Utils/SharpUtil.php b/src/Utils/SharpUtil.php index 3661da624..375f8831c 100644 --- a/src/Utils/SharpUtil.php +++ b/src/Utils/SharpUtil.php @@ -24,4 +24,12 @@ public function context(): SharpContext { return $this->context; } + + /** + * @internal + */ + public function reset() + { + $this->context = app(SharpContext::class); + } } From 3432fda7b87414a76f55fed1417200fe235b10e3 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 14 Jan 2026 15:49:57 +0100 Subject: [PATCH 07/12] wip --- src/Utils/Testing/DelegatesToResponse.php | 5 + src/Utils/Testing/Form/AssertableForm.php | 36 +++++ .../Testing/Form/FormatsDataForUpdate.php | 18 +++ src/Utils/Testing/Form/PendingForm.php | 74 ++++++++-- src/Utils/Testing/IsPendingComponent.php | 9 +- tests/Http/SharpAssertionsHttpTest.php | 129 +++++++++++++++++- 6 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 src/Utils/Testing/Form/AssertableForm.php create mode 100644 src/Utils/Testing/Form/FormatsDataForUpdate.php diff --git a/src/Utils/Testing/DelegatesToResponse.php b/src/Utils/Testing/DelegatesToResponse.php index 8d1a6bbdd..7493cda7b 100644 --- a/src/Utils/Testing/DelegatesToResponse.php +++ b/src/Utils/Testing/DelegatesToResponse.php @@ -19,4 +19,9 @@ public function __call(string $name, array $arguments) return $this; } + + public function __get(string $name) + { + return $this->response->{$name}; + } } diff --git a/src/Utils/Testing/Form/AssertableForm.php b/src/Utils/Testing/Form/AssertableForm.php new file mode 100644 index 000000000..f24f24259 --- /dev/null +++ b/src/Utils/Testing/Form/AssertableForm.php @@ -0,0 +1,36 @@ +pendingForm->store( + $this->formatDataForUpdate($this->pendingForm->form, $data, baseData: $this->formData()) + ); + } + + public function update(array $data = []): TestResponse + { + return $this->pendingForm->update( + $this->formatDataForUpdate($this->pendingForm->form, $data, baseData: $this->formData()) + ); + } + + public function formData(): array + { + return $this->response->inertiaProps('form.data'); + } +} diff --git a/src/Utils/Testing/Form/FormatsDataForUpdate.php b/src/Utils/Testing/Form/FormatsDataForUpdate.php new file mode 100644 index 000000000..44ed12938 --- /dev/null +++ b/src/Utils/Testing/Form/FormatsDataForUpdate.php @@ -0,0 +1,18 @@ +applyFormatters($data)) + ->when($baseData)->only(array_keys($data)) + ->all(), + ]; + } +} diff --git a/src/Utils/Testing/Form/PendingForm.php b/src/Utils/Testing/Form/PendingForm.php index b3d1703c6..aaae90f99 100644 --- a/src/Utils/Testing/Form/PendingForm.php +++ b/src/Utils/Testing/Form/PendingForm.php @@ -2,7 +2,7 @@ namespace Code16\Sharp\Utils\Testing\Form; -use Code16\Sharp\Show\SharpShow; +use Code16\Sharp\Form\SharpForm; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; use Code16\Sharp\Utils\Testing\GeneratesGlobalFilterUrl; @@ -13,11 +13,13 @@ class PendingForm { + use FormatsDataForUpdate; use GeneratesGlobalFilterUrl; use IsPendingComponent; - protected SharpShow $show; + public SharpForm $form; public string $entityKey; + protected bool $isSubsequentRequest = false; public function __construct( /** @var TestCase $test */ @@ -27,18 +29,72 @@ public function __construct( public PendingEntityList|PendingShow|null $parent = null, ) { $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); - $this->show = app(SharpEntityManager::class)->entityFor($this->entityKey)->getShowOrFail(); + $this->form = app(SharpEntityManager::class)->entityFor($this->entityKey)->getFormOrFail(); } - public function get(): TestResponse + public function create(): AssertableForm + { + $this->setGlobalFilterUrlDefault(); + + return new AssertableForm( + $this->test + ->get(route('code16.sharp.form.create', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + ])), + pendingForm: $this->forSubsequentRequest(), + ); + } + + public function edit(): AssertableForm + { + $this->setGlobalFilterUrlDefault(); + + return new AssertableForm( + $this->test + ->get(route('code16.sharp.form.edit', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + 'instanceId' => $this->instanceId, + ])), + pendingForm: $this->forSubsequentRequest(), + ); + } + + public function store(array $data): TestResponse { $this->setGlobalFilterUrlDefault(); return $this->test - ->get(route('code16.sharp.form.edit', [ - 'parentUri' => $this->getParentUri(), - 'entityKey' => $this->entityKey, - 'instanceId' => $this->instanceId, - ])); + ->post( + route('code16.sharp.form.store', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + ]), + $this->isSubsequentRequest ? $data : $this->formatDataForUpdate($this->form, $data), + ); + } + + public function update(array $data): TestResponse + { + $this->setGlobalFilterUrlDefault(); + + return $this->test + ->post( + route('code16.sharp.form.update', [ + 'parentUri' => $this->getParentUri(), + 'entityKey' => $this->entityKey, + 'instanceId' => $this->instanceId, + ]), + $this->isSubsequentRequest ? $data : $this->formatDataForUpdate($this->form, $data), + ); + } + + protected function forSubsequentRequest(): static + { + $pendingForm = clone $this; + $pendingForm->isSubsequentRequest = true; + + return $pendingForm; } } diff --git a/src/Utils/Testing/IsPendingComponent.php b/src/Utils/Testing/IsPendingComponent.php index 383e3b5fd..b981ec4ce 100644 --- a/src/Utils/Testing/IsPendingComponent.php +++ b/src/Utils/Testing/IsPendingComponent.php @@ -2,6 +2,7 @@ namespace Code16\Sharp\Utils\Testing; +use Code16\Sharp\Form\SharpSingleForm; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Links\BreadcrumbBuilder; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; @@ -32,13 +33,13 @@ protected function breadcrumbBuilder(array $components): BreadcrumbBuilder if ($first instanceof PendingShow && $first->instanceId) { $breadcrumb->appendEntityList($first->entityKey); } elseif ($first instanceof PendingForm) { - if ($first->instanceId) { + if ($first->form instanceof SharpSingleForm) { + $breadcrumb->appendSingleShowPage($first->entityKey); + } else { $breadcrumb->appendEntityList($first->entityKey); - if (app(SharpEntityManager::class)->entityFor($first->entityKey)->hasShow()) { + if (app(SharpEntityManager::class)->entityFor($first->entityKey)->hasShow() && $first->instanceId) { $breadcrumb->appendShowPage($first->entityKey, $first->instanceId); } - } else { - $breadcrumb->appendSingleShowPage($first->entityKey); } } diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index d4ca8df80..4ae017859 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -2,9 +2,11 @@ use Code16\Sharp\EntityList\Commands\EntityCommand; use Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand; +use Code16\Sharp\Form\Fields\SharpFormEditorField; use Code16\Sharp\Form\Fields\SharpFormTextField; use Code16\Sharp\Show\Fields\SharpShowEntityListField; use Code16\Sharp\Tests\Fixtures\Entities\PersonEntity; +use Code16\Sharp\Tests\Fixtures\Sharp\PersonForm; use Code16\Sharp\Tests\Fixtures\Sharp\PersonList; use Code16\Sharp\Tests\Fixtures\Sharp\PersonShow; use Code16\Sharp\Utils\Fields\FieldsContainer; @@ -202,8 +204,129 @@ public function getListData(): array ->assertListContains(['name' => 'Marie Curie']); }); -test('get & assert form', function () { +test('nested form', function () { + $this->sharpForm(PersonEntity::class)->create(); + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-form/person'); + $this->sharpForm(PersonEntity::class, 1)->update([]); + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-show/person/1/s-form/person/1'); +}); + +test('create & store form', function () { + fakeFormFor('person', new class() extends PersonForm + { + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields + ->addField(SharpFormEditorField::make('name')) + ->addField(SharpFormEditorField::make('job')); + } + + public function find($id): array + { + return ['name' => 'John Wayne', 'job' => 'actor']; + } + + public function update($id, array $data) + { + expect($data)->toEqual(['name' => 'John Doe', 'job' => null]); + + return 1; + } + }); + + $this->sharpForm(PersonEntity::class) + ->create() + ->assertOk() + ->store(['name' => 'John Doe']) + ->assertValid() + ->assertRedirect(); +}); + +test('store form', function () { + fakeFormFor('person', new class() extends PersonForm + { + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields + ->addField(SharpFormEditorField::make('name')) + ->addField(SharpFormEditorField::make('job')); + } + + public function find($id): array + { + return ['name' => 'John Wayne', 'job' => 'actor']; + } + + public function update($id, array $data) + { + expect($data)->toEqual(['name' => 'John Doe', 'job' => null]); + + return 1; + } + }); + + $this->sharpForm(PersonEntity::class) + ->store(['name' => 'John Doe']) + ->assertValid() + ->assertRedirect(); +}); + +test('edit & update form', function () { + fakeFormFor('person', new class() extends PersonForm + { + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields + ->addField(SharpFormEditorField::make('name')) + ->addField(SharpFormEditorField::make('job')); + } + + public function find($id): array + { + return ['name' => 'John Wayne', 'job' => 'actor']; + } + + public function update($id, array $data) + { + expect($data)->toEqual(['name' => 'John Doe', 'job' => 'actor']); + + return 1; + } + }); + $this->sharpForm(PersonEntity::class, 1) - ->get() - ->assertOk(); + ->edit() + ->assertOk() + ->update(['name' => 'John Doe']) + ->assertValid() + ->assertRedirect(); +}); + +test('update form', function () { + fakeFormFor('person', new class() extends PersonForm + { + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields + ->addField(SharpFormEditorField::make('name')) + ->addField(SharpFormEditorField::make('job')); + } + + public function find($id): array + { + return ['name' => 'John Wayne', 'job' => 'actor']; + } + + public function update($id, array $data) + { + expect($data)->toEqual(['name' => 'John Doe', 'job' => null]); + + return 1; + } + }); + + $this->sharpForm(PersonEntity::class, 1) + ->update(['name' => 'John Doe']) + ->assertValid() + ->assertRedirect(); }); From db1c3f569b5a82f77091462c332597fbe631dd73 Mon Sep 17 00:00:00 2001 From: antoine Date: Thu, 15 Jan 2026 15:42:06 +0100 Subject: [PATCH 08/12] wip --- src/Utils/Testing/Form/PendingForm.php | 14 ++ src/Utils/Testing/Show/PendingShow.php | 1 - tests/Http/SharpAssertionsHttpTest.php | 174 +++++++++++++++--- .../Utils/Testing/SharpAssertionsTest.php | 108 ----------- .../Utils/Testing/SharpAssertionsTestCase.php | 11 -- 5 files changed, 165 insertions(+), 143 deletions(-) delete mode 100644 tests/Unit/Utils/Testing/SharpAssertionsTest.php delete mode 100644 tests/Unit/Utils/Testing/SharpAssertionsTestCase.php diff --git a/src/Utils/Testing/Form/PendingForm.php b/src/Utils/Testing/Form/PendingForm.php index aaae90f99..da515272a 100644 --- a/src/Utils/Testing/Form/PendingForm.php +++ b/src/Utils/Testing/Form/PendingForm.php @@ -3,6 +3,7 @@ namespace Code16\Sharp\Utils\Testing\Form; use Code16\Sharp\Form\SharpForm; +use Code16\Sharp\Form\SharpSingleForm; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; use Code16\Sharp\Utils\Testing\GeneratesGlobalFilterUrl; @@ -10,6 +11,7 @@ use Code16\Sharp\Utils\Testing\Show\PendingShow; use Illuminate\Foundation\Testing\TestCase; use Illuminate\Testing\TestResponse; +use PHPUnit\Framework\Assert as PHPUnit; class PendingForm { @@ -36,6 +38,8 @@ public function create(): AssertableForm { $this->setGlobalFilterUrlDefault(); + PHPUnit::assertNotInstanceOf(SharpSingleForm::class, $this->form); + return new AssertableForm( $this->test ->get(route('code16.sharp.form.create', [ @@ -50,6 +54,10 @@ public function edit(): AssertableForm { $this->setGlobalFilterUrlDefault(); + if (! $this->form instanceof SharpSingleForm) { + PHPUnit::assertNotNull($this->instanceId, 'You can’t edit a form without an instance ID.'); + } + return new AssertableForm( $this->test ->get(route('code16.sharp.form.edit', [ @@ -65,6 +73,8 @@ public function store(array $data): TestResponse { $this->setGlobalFilterUrlDefault(); + PHPUnit::assertNotInstanceOf(SharpSingleForm::class, $this->form); + return $this->test ->post( route('code16.sharp.form.store', [ @@ -79,6 +89,10 @@ public function update(array $data): TestResponse { $this->setGlobalFilterUrlDefault(); + if (! $this->form instanceof SharpSingleForm) { + PHPUnit::assertNotNull($this->instanceId, 'You can’t update a form without an instance ID.'); + } + return $this->test ->post( route('code16.sharp.form.update', [ diff --git a/src/Utils/Testing/Show/PendingShow.php b/src/Utils/Testing/Show/PendingShow.php index 8b549b731..0e19df3d8 100644 --- a/src/Utils/Testing/Show/PendingShow.php +++ b/src/Utils/Testing/Show/PendingShow.php @@ -50,7 +50,6 @@ public function get(): AssertableShow $this->test ->get($this->show instanceof SharpSingleShow ? route('code16.sharp.single-show', [ - 'parentUri' => $this->getParentUri(), 'entityKey' => $this->entityKey, ]) : route('code16.sharp.show.show', [ diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index 4ae017859..b093a90bf 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -1,14 +1,17 @@ config()->declareEntity(PersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); }); it('get & assert an entity list', function () { @@ -39,7 +43,7 @@ public function getListData(): array }); it('call & assert an entity list entity command', function () { - fakeListFor('person', new class() extends PersonList + fakeListFor(PersonEntity::class, new class() extends PersonList { protected function getEntityCommands(): ?array { @@ -106,8 +110,8 @@ public function execute(array $data = []): array ->assertReturnsRefresh([1, 2]); }); -it('call & assert an entity list entity wiard command', function () { - fakeListFor('person', new class() extends PersonList +it('call & assert an entity list entity wizard command', function () { + fakeListFor(PersonEntity::class, new class() extends PersonList { protected function getEntityCommands(): ?array { @@ -154,8 +158,71 @@ protected function executeStepSecondStep(array $data): array ->assertReturnsReload(); }); -test('get & assert show', function () { - fakeShowFor('person', new class() extends PersonShow +it('call & assert a show instance command', function () { + fakeShowFor(PersonEntity::class, new class() extends PersonShow + { + public function getInstanceCommands(): ?array + { + return [ + 'cmd' => new class() extends InstanceCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('action')); + } + + public function execute($instanceId, 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'), + }; + } + }, + ]; + } + }); + + $this->sharpShow(PersonEntity::class, 1) + ->callInstanceCommand('cmd', ['action' => 'info']) + ->assertReturnsInfo('ok'); + + $this->sharpShow(PersonEntity::class, 1) + ->callInstanceCommand('cmd', ['action' => 'link']) + ->assertReturnsLink('https://example.org'); + + $this->sharpShow(PersonEntity::class, 1) + ->callInstanceCommand('cmd', ['action' => 'view']) + ->assertReturnsView('fixtures::test', [ + 'text' => 'text', + ]); + + $this->sharpShow(PersonEntity::class, 1) + ->callInstanceCommand('cmd', ['action' => 'reload']) + ->assertReturnsReload(); + + $this->sharpShow(PersonEntity::class, 1) + ->callInstanceCommand('cmd', ['action' => 'download']) + ->assertReturnsDownload('account.pdf'); +}); + +test('get show', function () { + fakeShowFor(PersonEntity::class, new class() extends PersonShow { public function find($id): array { @@ -167,10 +234,30 @@ public function find($id): array ->get() ->assertOk() ->assertShowData(['name' => 'John Doe']); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-show/person/1'); +}); + +test('get single show', function () { + + fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow + { + public function findSingle(): array + { + return ['name' => 'John Doe', 'age' => 31]; + } + }); + + $this->sharpShow(SinglePersonEntity::class) + ->get() + ->assertOk() + ->assertShowData(['name' => 'John Doe']); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-show/single-person'); }); -test('get & assert show EEL', function () { - fakeShowFor('person', new class() extends PersonShow +test('get show EEL', function () { + fakeShowFor(PersonEntity::class, new class() extends PersonShow { public function buildShowFields(FieldsContainer $showFields): void { @@ -204,15 +291,44 @@ public function getListData(): array ->assertListContains(['name' => 'Marie Curie']); }); -test('nested form', function () { - $this->sharpForm(PersonEntity::class)->create(); - expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-form/person'); - $this->sharpForm(PersonEntity::class, 1)->update([]); - expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-show/person/1/s-form/person/1'); +test('get nested show', function () { + fakeShowFor(PersonEntity::class, new class() extends PersonShow + { + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField( + SharpShowEntityListField::make(PersonEntity::class) + ); + } + + public function find($id): array + { + return ['name' => 'John Doe', 'age' => 31]; + } + }); + + fakeListFor(PersonEntity::class, new class() extends PersonList + { + public function getListData(): array + { + return [ + ['id' => 1, 'name' => 'Marie Curie'], + ['id' => 2, 'name' => 'Albert Einstein'], + ]; + } + }); + + $this->sharpShow(PersonEntity::class, 1) + ->sharpListField(PersonEntity::class) + ->sharpShow(PersonEntity::class, 2) + ->get() + ->assertOk(); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-show/person/1/s-show/person/2'); }); test('create & store form', function () { - fakeFormFor('person', new class() extends PersonForm + fakeFormFor(PersonEntity::class, new class() extends PersonForm { public function buildFormFields(FieldsContainer $formFields): void { @@ -221,14 +337,14 @@ public function buildFormFields(FieldsContainer $formFields): void ->addField(SharpFormEditorField::make('job')); } - public function find($id): array + public function create(): array { return ['name' => 'John Wayne', 'job' => 'actor']; } public function update($id, array $data) { - expect($data)->toEqual(['name' => 'John Doe', 'job' => null]); + expect($data)->toEqual(['name' => 'John Doe', 'job' => 'actor']); return 1; } @@ -240,10 +356,12 @@ public function update($id, array $data) ->store(['name' => 'John Doe']) ->assertValid() ->assertRedirect(); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-form/person'); }); test('store form', function () { - fakeFormFor('person', new class() extends PersonForm + fakeFormFor(PersonEntity::class, new class() extends PersonForm { public function buildFormFields(FieldsContainer $formFields): void { @@ -252,11 +370,6 @@ public function buildFormFields(FieldsContainer $formFields): void ->addField(SharpFormEditorField::make('job')); } - public function find($id): array - { - return ['name' => 'John Wayne', 'job' => 'actor']; - } - public function update($id, array $data) { expect($data)->toEqual(['name' => 'John Doe', 'job' => null]); @@ -269,10 +382,12 @@ public function update($id, array $data) ->store(['name' => 'John Doe']) ->assertValid() ->assertRedirect(); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-form/person'); }); test('edit & update form', function () { - fakeFormFor('person', new class() extends PersonForm + fakeFormFor(PersonEntity::class, new class() extends PersonForm { public function buildFormFields(FieldsContainer $formFields): void { @@ -300,10 +415,12 @@ public function update($id, array $data) ->update(['name' => 'John Doe']) ->assertValid() ->assertRedirect(); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-show/person/1/s-form/person/1'); }); test('update form', function () { - fakeFormFor('person', new class() extends PersonForm + fakeFormFor(PersonEntity::class, new class() extends PersonForm { public function buildFormFields(FieldsContainer $formFields): void { @@ -329,4 +446,15 @@ public function update($id, array $data) ->update(['name' => 'John Doe']) ->assertValid() ->assertRedirect(); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-list/person/s-show/person/1/s-form/person/1'); +}); + +test('update single form', function () { + $this->sharpForm(SinglePersonEntity::class) + ->update(['name' => 'John Doe']) + ->assertValid() + ->assertRedirect(); + + expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-show/single-person/s-form/single-person'); }); diff --git a/tests/Unit/Utils/Testing/SharpAssertionsTest.php b/tests/Unit/Utils/Testing/SharpAssertionsTest.php deleted file mode 100644 index d75e7ee5a..000000000 --- a/tests/Unit/Utils/Testing/SharpAssertionsTest.php +++ /dev/null @@ -1,108 +0,0 @@ -config()->declareEntity(PersonEntity::class); - - 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', - ]; - } - }, - ]; - } - }); -}); - -it('allows to test entity list', function () { - /** @var \Mockery\MockInterface|SharpAssertionsTestCase $testMock */ - $testMock = Mockery::mock(SharpAssertionsTestCase::class)->makePartial(); - - $testMock->shouldReceive('get') - ->once() - ->with(route('code16.sharp.list', [ - 'entityKey' => 'person', - 'filter_job' => 'physicist', - ])) - ->andReturn(new TestResponse(new Response())); - - $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, - ] - ) - ->andReturn(new TestResponse(new Response())); - - $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, - ] - ) - ->andReturn(new TestResponse(new Response())); - - $testMock->sharpList('person') - ->withFilter('job', 'physicist') - ->callEntityCommand('test', ['foo' => 'bar']) - ->assertOk(); -}); diff --git a/tests/Unit/Utils/Testing/SharpAssertionsTestCase.php b/tests/Unit/Utils/Testing/SharpAssertionsTestCase.php deleted file mode 100644 index 3e392c2c4..000000000 --- a/tests/Unit/Utils/Testing/SharpAssertionsTestCase.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Fri, 16 Jan 2026 15:52:26 +0100 Subject: [PATCH 09/12] wip --- .../Traits/HandleEntityCommands.php | 3 + .../Testing/Commands/AssertableCommand.php | 24 +- .../Commands/AssertableCommandForm.php | 44 ++++ .../Commands/FormatsDataForCommand.php | 18 ++ src/Utils/Testing/Commands/PendingCommand.php | 45 ++++ .../Testing/Dashboard/AssertableDashboard.php | 24 ++ .../Testing/Dashboard/PendingDashboard.php | 137 ++++++++++ .../Testing/EntityList/PendingEntityList.php | 107 +++++--- src/Utils/Testing/SharpAssertions.php | 6 + src/Utils/Testing/Show/PendingShow.php | 53 ++-- tests/Http/SharpAssertionsHttpTest.php | 239 ++++++++++++++---- 11 files changed, 591 insertions(+), 109 deletions(-) create mode 100644 src/Utils/Testing/Commands/AssertableCommandForm.php create mode 100644 src/Utils/Testing/Commands/FormatsDataForCommand.php create mode 100644 src/Utils/Testing/Commands/PendingCommand.php create mode 100644 src/Utils/Testing/Dashboard/AssertableDashboard.php create mode 100644 src/Utils/Testing/Dashboard/PendingDashboard.php diff --git a/src/EntityList/Traits/HandleEntityCommands.php b/src/EntityList/Traits/HandleEntityCommands.php index 87a56e91a..c2bd7f9cf 100644 --- a/src/EntityList/Traits/HandleEntityCommands.php +++ b/src/EntityList/Traits/HandleEntityCommands.php @@ -45,6 +45,8 @@ protected function appendEntityCommandsToConfig(array &$config): void final public function getEntityCommandsHandlers(): Collection { + ray($this->entityCommandHandlers); + ray()->trace(); if ($this->entityCommandHandlers === null) { $groupIndex = 0; $this->entityCommandHandlers = collect($this->getEntityCommands()) @@ -73,6 +75,7 @@ final public function getEntityCommandsHandlers(): Collection $commandHandler->setCommandKey($commandKey); } + // ray($this->queryParams ?? null, $commandHandler)->trace(); if (isset($this->queryParams)) { // We have to init query params of the command $commandHandler->initQueryParams($this->queryParams); diff --git a/src/Utils/Testing/Commands/AssertableCommand.php b/src/Utils/Testing/Commands/AssertableCommand.php index 4dc7873f0..c605c3c72 100644 --- a/src/Utils/Testing/Commands/AssertableCommand.php +++ b/src/Utils/Testing/Commands/AssertableCommand.php @@ -23,6 +23,8 @@ class AssertableCommand public function __construct( /** @var Closure(array,string): TestResponse */ protected Closure $postCommand, + /** @var Closure(?string): TestResponse */ + protected Closure $getForm, protected SharpEntityList|SharpShow|SharpDashboard $commandContainer, protected array $data = [], protected ?string $step = null, @@ -56,7 +58,7 @@ public function assertViewIs($value) public function assertReturnsView(?string $view = null, ?array $data = null): static { - $this->response->assertJson(fn (AssertableJson $json) => $json + $this->response->assertOk()->assertJson(fn (AssertableJson $json) => $json ->where('action', 'view') ->etc() ); @@ -74,7 +76,7 @@ public function assertReturnsView(?string $view = null, ?array $data = null): st public function assertReturnsInfo(string $message = ''): static { - $this->response->assertJson(fn (AssertableJson $json) => $json + $this->response->assertOk()->assertJson(fn (AssertableJson $json) => $json ->where('action', 'info') ->when($message)->where('message', $message) ->etc() @@ -85,7 +87,7 @@ public function assertReturnsInfo(string $message = ''): static public function assertReturnsLink(string $url = ''): static { - $this->response->assertJson(fn (AssertableJson $json) => $json + $this->response->assertOk()->assertJson(fn (AssertableJson $json) => $json ->where('action', 'link') ->when($url)->where('link', $url) ->etc() @@ -96,7 +98,7 @@ public function assertReturnsLink(string $url = ''): static public function assertReturnsReload(): static { - $this->response->assertJson(fn (AssertableJson $json) => $json + $this->response->assertOk()->assertJson(fn (AssertableJson $json) => $json ->where('action', 'reload') ->etc() ); @@ -106,7 +108,7 @@ public function assertReturnsReload(): static public function assertReturnsRefresh(array $ids): static { - $this->response->assertJson(fn (AssertableJson $json) => $json + $this->response->assertOk()->assertJson(fn (AssertableJson $json) => $json ->where('action', 'refresh') ->etc() ); @@ -121,7 +123,7 @@ public function assertReturnsRefresh(array $ids): static public function assertReturnsStep(?string $step = null): static { - $this->response->assertJson(fn (AssertableJson $json) => $json + $this->response->assertOk()->assertJson(fn (AssertableJson $json) => $json ->where('action', 'step') ->etc() ); @@ -135,7 +137,7 @@ public function assertReturnsStep(?string $step = null): static public function assertReturnsDownload(?string $filename = null): static { - $this->response->assertStreamed(); + $this->response->assertOk()->assertStreamed(); if ($filename) { preg_match('/filename="?([^";]+)"?/', $this->response->headers->get('Content-Disposition'), $matches); @@ -145,14 +147,14 @@ public function assertReturnsDownload(?string $filename = null): static return $this; } - public function callNextStep(array $data = []): static + public function getNextStepForm(): AssertableCommandForm { $this->assertReturnsStep(); - return new AssertableCommand( - $this->postCommand, + return new AssertableCommandForm( + post: $this->postCommand, + getForm: $this->getForm, commandContainer: $this->commandContainer, - data: $data, step: $this->response->json('step'), ); } diff --git a/src/Utils/Testing/Commands/AssertableCommandForm.php b/src/Utils/Testing/Commands/AssertableCommandForm.php new file mode 100644 index 000000000..df134e8a6 --- /dev/null +++ b/src/Utils/Testing/Commands/AssertableCommandForm.php @@ -0,0 +1,44 @@ +response = ($this->getForm)($this->step); + } + + public function post(array $data = []): AssertableCommand + { + ray($this->formData()); + + return new AssertableCommand( + postCommand: fn ($data, $step) => ($this->post)($data, $step, $this->formData()), + getForm: $this->getForm, + commandContainer: $this->commandContainer, + data: $data, + step: $this->step, + ); + } + + public function formData(): ?array + { + return $this->response->json('data'); + } +} diff --git a/src/Utils/Testing/Commands/FormatsDataForCommand.php b/src/Utils/Testing/Commands/FormatsDataForCommand.php new file mode 100644 index 000000000..f790ff89b --- /dev/null +++ b/src/Utils/Testing/Commands/FormatsDataForCommand.php @@ -0,0 +1,18 @@ +applyFormatters($data)) + ->when($baseData)->only(array_keys($data)) + ->all(), + ]; + } +} diff --git a/src/Utils/Testing/Commands/PendingCommand.php b/src/Utils/Testing/Commands/PendingCommand.php new file mode 100644 index 000000000..dc713078b --- /dev/null +++ b/src/Utils/Testing/Commands/PendingCommand.php @@ -0,0 +1,45 @@ +setGlobalFilterUrlDefault(); + + return new AssertableCommandForm( + post: $this->post, + getForm: $this->getForm, + commandContainer: $this->commandContainer, + step: $this->step + ); + } + + public function post(): AssertableCommand + { + $this->setGlobalFilterUrlDefault(); + + return new AssertableCommand( + postCommand: $this->post, + getForm: $this->getForm, + commandContainer: $this->commandContainer, + step: $this->step + ); + } +} diff --git a/src/Utils/Testing/Dashboard/AssertableDashboard.php b/src/Utils/Testing/Dashboard/AssertableDashboard.php new file mode 100644 index 000000000..94f623eb7 --- /dev/null +++ b/src/Utils/Testing/Dashboard/AssertableDashboard.php @@ -0,0 +1,24 @@ +pendingDashboard->parent instanceof PendingShow + ? $this->response->json('data') + : $this->response->inertiaProps('dashboard.data'); + } +} diff --git a/src/Utils/Testing/Dashboard/PendingDashboard.php b/src/Utils/Testing/Dashboard/PendingDashboard.php new file mode 100644 index 000000000..fe52f1d58 --- /dev/null +++ b/src/Utils/Testing/Dashboard/PendingDashboard.php @@ -0,0 +1,137 @@ +entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); + $this->dashboard = app(SharpEntityManager::class)->entityFor($this->entityKey)->getViewOrFail(); + } + + public function withFilter(string $filterKey, mixed $value): static + { + $key = $this->dashboard->filterContainer()->findFilterHandler($filterKey)->getKey(); + $this->filterValues[$key] = $value; + + return $this; + } + + public function get(): AssertableDashboard + { + $this->setGlobalFilterUrlDefault(); + + return new AssertableDashboard( + $this->parent instanceof PendingShow + ? $this->test + ->getJson( + route('code16.sharp.api.dashboard', [ + 'dashboardKey' => $this->entityKey, + ...$this->dashboard + ->filterContainer() + ->getQueryParamsFromFilterValues($this->dashboardShowField()->toArray()['hiddenFilters'] ?? []), + ...$this->dashboardQueryParams(), + ]), + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] + ) + : $this->test->get(route('code16.sharp.dashboard', [ + 'dashboardKey' => $this->entityKey, + ...$this->dashboardQueryParams(), + ])), + $this, + ); + } + + public function dashboardCommand(string $commandKeyOrClassName): PendingCommand + { + $this->setGlobalFilterUrlDefault(); + + $commandKey = class_exists($commandKeyOrClassName) + ? class_basename($commandKeyOrClassName) + : $commandKeyOrClassName; + + return new PendingCommand( + getForm: fn (?string $step = null) => $this + ->test + ->getJson( + route( + 'code16.sharp.api.dashboard.command.form', + [ + 'entityKey' => $this->entityKey, + 'commandKey' => $commandKey, + 'command_step' => $step, + ...$this->dashboardQueryParams(), + ] + ), + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] + ), + post: fn (array $data, ?string $step = null, ?array $baseData = null) => $this + ->test + ->postJson( + route( + 'code16.sharp.api.dashboard.command', + ['entityKey' => $this->entityKey, 'commandKey' => $commandKey] + ), + [ + 'data' => $this->formatDataForCommand( + $this->dashboard->findDashboardCommandHandler($commandKey), + $data, + $baseData + ), + 'query' => $this->dashboardQueryParams(), + 'command_step' => $step, + ], + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] + ), + commandContainer: $this->dashboard, + ); + } + + protected function dashboardShowField(): ?SharpShowDashboardField + { + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->parent instanceof PendingShow + ? $this->parent->show->getBuiltFields() + ->first(fn (SharpShowField $field) => $field instanceof SharpShowDashboardField + && $field->toArray()['dashboardKey'] === $this->entityKey + ) + : null; + } + + protected function dashboardQueryParams(): array + { + return $this->dashboard + ->filterContainer() + ->getQueryParamsFromFilterValues($this->filterValues) + ->all(); + } +} diff --git a/src/Utils/Testing/EntityList/PendingEntityList.php b/src/Utils/Testing/EntityList/PendingEntityList.php index 9259ee712..43167bd75 100644 --- a/src/Utils/Testing/EntityList/PendingEntityList.php +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -7,7 +7,8 @@ use Code16\Sharp\Show\Fields\SharpShowEntityListField; use Code16\Sharp\Show\Fields\SharpShowField; use Code16\Sharp\Utils\Entities\SharpEntityManager; -use Code16\Sharp\Utils\Testing\Commands\AssertableCommand; +use Code16\Sharp\Utils\Testing\Commands\FormatsDataForCommand; +use Code16\Sharp\Utils\Testing\Commands\PendingCommand; use Code16\Sharp\Utils\Testing\Form\PendingForm; use Code16\Sharp\Utils\Testing\IsPendingComponent; use Code16\Sharp\Utils\Testing\Show\PendingShow; @@ -16,6 +17,7 @@ class PendingEntityList { + use FormatsDataForCommand; use IsPendingComponent; protected SharpEntityList $entityList; @@ -61,95 +63,122 @@ public function get(): AssertableEntityList return new AssertableEntityList( $this->parent instanceof PendingShow ? $this->test - ->withHeader( - SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->getCurrentPageUrlFromParents(), - ) - ->get( + ->getJson( route('code16.sharp.api.list', [ 'entityKey' => $this->entityKey, ...$this->entityList ->filterContainer() ->getQueryParamsFromFilterValues($this->entityListShowField()->toArray()['hiddenFilters'] ?? []), ...$this->entityListQueryParams(), - ]) + ]), + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] ) : $this->test->get(route('code16.sharp.list', [ - 'entityKey' => $this->entityKey, - ...$this->entityListQueryParams(), - ])), + 'entityKey' => $this->entityKey, + ...$this->entityListQueryParams(), + ])), $this, ); } - public function callEntityCommand( - string $commandKeyOrClassName, - array $data = [], - ?string $commandStep = null - ): AssertableCommand { + public function entityCommand(string $commandKeyOrClassName): PendingCommand + { $this->setGlobalFilterUrlDefault(); $commandKey = class_exists($commandKeyOrClassName) ? class_basename($commandKeyOrClassName) : $commandKeyOrClassName; - return new AssertableCommand( - fn ($data, $step) => $this + return new PendingCommand( + getForm: fn (?string $step = null) => $this + ->test + ->getJson( + route( + 'code16.sharp.api.list.command.entity.form', + [ + 'entityKey' => $this->entityKey, + 'commandKey' => $commandKey, + 'command_step' => $step, + ...$this->entityListQueryParams(), + ] + ), + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] + ), + post: fn (array $data, ?string $step = null, ?array $baseData = null) => $this ->test - ->withHeader( - SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->getCurrentPageUrlFromParents(), - ) ->postJson( route( 'code16.sharp.api.list.command.entity', ['entityKey' => $this->entityKey, 'commandKey' => $commandKey] ), [ - 'data' => $data, + 'data' => $this->formatDataForCommand( + $this->entityList->findEntityCommandHandler($commandKey), + $data, + $baseData + ), 'query' => $this->entityListQueryParams(), 'command_step' => $step, ], + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] ), commandContainer: $this->entityList, - data: $data, - step: $commandStep ); } - public function callInstanceCommand( - int|string $instanceId, - string $commandKeyOrClassName, - array $data = [], - ?string $commandStep = null - ): AssertableCommand { + public function instanceCommand(string $commandKeyOrClassName, int|string $instanceId): PendingCommand + { $this->setGlobalFilterUrlDefault(); $commandKey = class_exists($commandKeyOrClassName) ? class_basename($commandKeyOrClassName) : $commandKeyOrClassName; - return new AssertableCommand( - fn ($data, $step) => $this + return new PendingCommand( + getForm: fn (?string $step = null) => $this + ->test + ->getJson( + route( + 'code16.sharp.api.list.command.instance.form', + [ + 'entityKey' => $this->entityKey, + 'instanceId' => $instanceId, + 'commandKey' => $commandKey, + 'command_step' => $step, + ...$this->entityListQueryParams(), + ] + ), + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] + ), + post: fn (array $data, ?string $step = null, ?array $baseData = null) => $this ->test - ->withHeader( - SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->getCurrentPageUrlFromParents(), - ) ->postJson( route( 'code16.sharp.api.list.command.instance', ['entityKey' => $this->entityKey, 'instanceId' => $instanceId, 'commandKey' => $commandKey] ), [ - 'data' => $data, + 'data' => $this->formatDataForCommand( + $this->entityList->findInstanceCommandHandler($commandKey), + $data, + $baseData + ), 'query' => $this->entityListQueryParams(), 'command_step' => $step, ], + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] ), commandContainer: $this->entityList, - data: $data, - step: $commandStep, ); } diff --git a/src/Utils/Testing/SharpAssertions.php b/src/Utils/Testing/SharpAssertions.php index d97175721..6d2ab40ed 100644 --- a/src/Utils/Testing/SharpAssertions.php +++ b/src/Utils/Testing/SharpAssertions.php @@ -6,6 +6,7 @@ use Code16\Sharp\Http\Context\SharpBreadcrumb; use Code16\Sharp\Utils\Entities\SharpEntityManager; use Code16\Sharp\Utils\Links\BreadcrumbBuilder; +use Code16\Sharp\Utils\Testing\Dashboard\PendingDashboard; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; use Code16\Sharp\Utils\Testing\Form\PendingForm; use Code16\Sharp\Utils\Testing\Show\PendingShow; @@ -32,6 +33,11 @@ public function sharpForm(string $entityClassNameOrKey, int|string|null $instanc return new PendingForm($this, $entityClassNameOrKey, $instanceId); } + public function sharpDashboard(string $entityClassNameOrKey): PendingDashboard + { + return new PendingDashboard($this, $entityClassNameOrKey); + } + /** * @deprecated use withSharpBreadcrumb() instead */ diff --git a/src/Utils/Testing/Show/PendingShow.php b/src/Utils/Testing/Show/PendingShow.php index 0e19df3d8..3ac412f6d 100644 --- a/src/Utils/Testing/Show/PendingShow.php +++ b/src/Utils/Testing/Show/PendingShow.php @@ -6,7 +6,9 @@ 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\Commands\FormatsDataForCommand; +use Code16\Sharp\Utils\Testing\Commands\PendingCommand; +use Code16\Sharp\Utils\Testing\Dashboard\PendingDashboard; use Code16\Sharp\Utils\Testing\EntityList\PendingEntityList; use Code16\Sharp\Utils\Testing\Form\PendingForm; use Code16\Sharp\Utils\Testing\GeneratesGlobalFilterUrl; @@ -15,6 +17,7 @@ class PendingShow { + use FormatsDataForCommand; use GeneratesGlobalFilterUrl; use IsPendingComponent; @@ -42,6 +45,11 @@ public function sharpListField(string $entityClassNameOrKey): PendingEntityList return new PendingEntityList($this->test, $entityClassNameOrKey, parent: $this); } + public function sharpDashboardField(string $entityClassNameOrKey): PendingDashboard + { + return new PendingDashboard($this->test, $entityClassNameOrKey, parent: $this); + } + public function get(): AssertableShow { $this->setGlobalFilterUrlDefault(); @@ -61,37 +69,52 @@ public function get(): AssertableShow ); } - public function callInstanceCommand( - string $commandKeyOrClassName, - array $data = [], - ?string $commandStep = null, - ): AssertableCommand { + public function instanceCommand(string $commandKeyOrClassName): PendingCommand + { $this->setGlobalFilterUrlDefault(); $commandKey = class_exists($commandKeyOrClassName) ? class_basename($commandKeyOrClassName) : $commandKeyOrClassName; - return new AssertableCommand( - fn ($data, $step) => $this + return new PendingCommand( + getForm: fn (?string $step = null) => $this + ->test + ->getJson( + $this->show instanceof SharpSingleShow + ? route( + 'code16.sharp.api.show.command.singleInstance.form', + ['entityKey' => $this->entityKey, 'commandKey' => $commandKey, 'command_step' => $step] + ) + : route( + 'code16.sharp.api.show.command.instance.form', + [ + 'entityKey' => $this->entityKey, + 'instanceId' => $this->instanceId, + 'commandKey' => $commandKey, + 'command_step' => $step, + ] + ), + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] + ), + post: fn (?array $data, ?string $step = null, ?array $baseData = null) => $this ->test - ->withHeader( - SharpBreadcrumb::CURRENT_PAGE_URL_HEADER, - $this->getCurrentPageUrlFromParents(), - ) ->postJson( route( 'code16.sharp.api.show.command.instance', ['entityKey' => $this->entityKey, 'instanceId' => $this->instanceId, 'commandKey' => $commandKey] ), [ - 'data' => $data, + 'data' => $this->formatDataForCommand($this->show->findInstanceCommandHandler($commandKey), $data, $baseData), 'command_step' => $step, ], + headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => $this->getCurrentPageUrlFromParents(), + ] ), commandContainer: $this->show, - data: $data, - step: $commandStep, ); } } diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index b093a90bf..bdd35cd11 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -5,13 +5,16 @@ use Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand; use Code16\Sharp\Form\Fields\SharpFormEditorField; use Code16\Sharp\Form\Fields\SharpFormTextField; +use Code16\Sharp\Show\Fields\SharpShowDashboardField; use Code16\Sharp\Show\Fields\SharpShowEntityListField; +use Code16\Sharp\Tests\Fixtures\Entities\DashboardEntity; use Code16\Sharp\Tests\Fixtures\Entities\PersonEntity; use Code16\Sharp\Tests\Fixtures\Entities\SinglePersonEntity; use Code16\Sharp\Tests\Fixtures\Sharp\PersonForm; use Code16\Sharp\Tests\Fixtures\Sharp\PersonList; use Code16\Sharp\Tests\Fixtures\Sharp\PersonShow; use Code16\Sharp\Tests\Fixtures\Sharp\SinglePersonShow; +use Code16\Sharp\Tests\Fixtures\Sharp\TestDashboard; use Code16\Sharp\Utils\Fields\FieldsContainer; use Code16\Sharp\Utils\Testing\SharpAssertions; use Illuminate\Http\UploadedFile; @@ -45,10 +48,23 @@ public function getListData(): array it('call & assert an entity list entity command', function () { fakeListFor(PersonEntity::class, new class() extends PersonList { + protected function getFilters(): ?array + { + return [ + new class() extends \Code16\Sharp\Filters\CheckFilter + { + public function buildFilterConfig(): void + { + $this->configureKey('is_valid'); + } + }, + ]; + } + protected function getEntityCommands(): ?array { return [ - 'cmd' => new class() extends EntityCommand + 'cmd-form' => new class() extends EntityCommand { public function label(): ?string { @@ -57,11 +73,20 @@ public function label(): ?string public function buildFormFields(FieldsContainer $formFields): void { - $formFields->addField(SharpFormTextField::make('action')); + $formFields + ->addField(SharpFormTextField::make('action')) + ->addField(SharpFormTextField::make('field_with_initial_value')); + } + + protected function initialData(): array + { + return ['field_with_initial_value' => 'test']; } public function execute(array $data = []): array { + expect($data)->toMatchArray(['field_with_initial_value' => 'test']); + if ($data['action'] === 'download') { Storage::fake('files'); UploadedFile::fake() @@ -79,34 +104,55 @@ public function execute(array $data = []): array }; } }, + 'cmd' => new class() extends EntityCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function execute(array $data = []): array + { + expect($this->queryParams->filterFor('is_valid'))->toBeTrue(); + + return $this->info('ok'); + } + }, ]; } }); + $this->withoutExceptionHandling(); + $this->sharpList(PersonEntity::class) - ->callEntityCommand('cmd', ['action' => 'info']) + ->withFilter('is_valid', true) + ->entityCommand('cmd')->post() ->assertReturnsInfo('ok'); $this->sharpList(PersonEntity::class) - ->callEntityCommand('cmd', ['action' => 'link']) + ->entityCommand('cmd-form')->getForm()->post(['action' => 'info']) + ->assertReturnsInfo('ok'); + + $this->sharpList(PersonEntity::class) + ->entityCommand('cmd-form')->getForm()->post(['action' => 'link']) ->assertReturnsLink('https://example.org'); $this->sharpList(PersonEntity::class) - ->callEntityCommand('cmd', ['action' => 'view']) + ->entityCommand('cmd-form')->getForm()->post(['action' => 'view']) ->assertReturnsView('fixtures::test', [ 'text' => 'text', ]); $this->sharpList(PersonEntity::class) - ->callEntityCommand('cmd', ['action' => 'reload']) + ->entityCommand('cmd-form')->getForm()->post(['action' => 'reload']) ->assertReturnsReload(); $this->sharpList(PersonEntity::class) - ->callEntityCommand('cmd', ['action' => 'download']) + ->entityCommand('cmd-form')->getForm()->post(['action' => 'download']) ->assertReturnsDownload('account.pdf'); $this->sharpList(PersonEntity::class) - ->callEntityCommand('cmd', ['action' => 'refresh']) + ->entityCommand('cmd-form')->getForm()->post(['action' => 'refresh']) ->assertReturnsRefresh([1, 2]); }); @@ -123,26 +169,42 @@ public function label(): ?string return 'my command'; } + protected function initialDataForFirstStep(): array + { + return ['field_with_initial_value' => 'test']; + } + public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void { - $formFields->addField(SharpFormTextField::make('name')); + $formFields + ->addField(SharpFormTextField::make('name')) + ->addField(SharpFormTextField::make('field_with_initial_value')); } protected function executeFirstStep(array $data): array { + expect($data)->toEqual(['name' => 'John', 'field_with_initial_value' => 'test']); + $this->validate($data, ['name' => 'required']); return $this->toStep('second-step'); } + public function initialDataForStepSecondStep(): array + { + return ['field_with_initial_value' => 'test']; + } + public function buildFormFieldsForStepSecondStep(FieldsContainer $formFields): void { - $formFields->addField(SharpFormTextField::make('age')); + $formFields + ->addField(SharpFormTextField::make('age')) + ->addField(SharpFormTextField::make('field_with_initial_value')); } protected function executeStepSecondStep(array $data): array { - expect($data)->toEqual(['age' => 30]); + expect($data)->toEqual(['field_with_initial_value' => 'test', 'age' => 30]); return $this->reload(); } @@ -151,20 +213,23 @@ protected function executeStepSecondStep(array $data): array } }); + $this->withoutExceptionHandling(); + $this->sharpList(PersonEntity::class) - ->callEntityCommand('wizard', ['name' => 'John']) + ->entityCommand('wizard') + ->getForm()->post(['name' => 'John']) ->assertReturnsStep('second-step') - ->callNextStep(['age' => 30]) + ->getNextStepForm()->post(['age' => 30]) ->assertReturnsReload(); }); -it('call & assert a show instance command', function () { - fakeShowFor(PersonEntity::class, new class() extends PersonShow +it('call & assert a entity list instance command', function () { + fakeListFor(PersonEntity::class, new class() extends PersonList { - public function getInstanceCommands(): ?array + protected function getInstanceCommands(): ?array { return [ - 'cmd' => new class() extends InstanceCommand + 'cmd-form' => new class() extends InstanceCommand { public function label(): ?string { @@ -178,47 +243,84 @@ public function buildFormFields(FieldsContainer $formFields): void public function execute($instanceId, 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'), + 'info' => $this->info('instance '.$instanceId), }; } }, + 'cmd' => new class() extends InstanceCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function execute($instanceId, array $data = []): array + { + return $this->info('instance '.$instanceId); + } + }, ]; } }); - $this->sharpShow(PersonEntity::class, 1) - ->callInstanceCommand('cmd', ['action' => 'info']) - ->assertReturnsInfo('ok'); + $this->sharpList(PersonEntity::class) + ->instanceCommand('cmd', 1)->post() + ->assertReturnsInfo('instance 1'); - $this->sharpShow(PersonEntity::class, 1) - ->callInstanceCommand('cmd', ['action' => 'link']) - ->assertReturnsLink('https://example.org'); + $this->sharpList(PersonEntity::class) + ->instanceCommand('cmd-form', 1)->getForm()->post(['action' => 'info']) + ->assertReturnsInfo('instance 1'); +}); - $this->sharpShow(PersonEntity::class, 1) - ->callInstanceCommand('cmd', ['action' => 'view']) - ->assertReturnsView('fixtures::test', [ - 'text' => 'text', - ]); +it('call & assert a show instance command', function () { + fakeShowFor(PersonEntity::class, new class() extends PersonShow + { + public function getInstanceCommands(): ?array + { + return [ + 'cmd-form' => new class() extends InstanceCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('action')); + } + + public function execute($instanceId, array $data = []): array + { + return match ($data['action']) { + 'info' => $this->info('instance '.$instanceId), + }; + } + }, + 'cmd' => new class() extends InstanceCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function execute($instanceId, array $data = []): array + { + return $this->info('instance '.$instanceId); + } + }, + ]; + } + }); $this->sharpShow(PersonEntity::class, 1) - ->callInstanceCommand('cmd', ['action' => 'reload']) - ->assertReturnsReload(); + ->instanceCommand('cmd')->post() + ->assertReturnsInfo('instance 1'); $this->sharpShow(PersonEntity::class, 1) - ->callInstanceCommand('cmd', ['action' => 'download']) - ->assertReturnsDownload('account.pdf'); + ->instanceCommand('cmd-form')->getForm()->post(['action' => 'info']) + ->assertReturnsInfo('instance 1'); }); test('get show', function () { @@ -458,3 +560,52 @@ public function update($id, array $data) expect(sharp()->context()->breadcrumb()->getCurrentPath())->toEqual('s-show/single-person/s-form/single-person'); }); + +test('get dashboard', function () { + + sharp()->config()->declareEntity(DashboardEntity::class); + + fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard + { + protected function buildWidgetsData(): void + { + $this->setPanelData('panel', ['name' => 'Marie Curie']); + } + }); + + $this->sharpDashboard(DashboardEntity::class) + ->get() + ->assertOk(); +}); + +test('get show dashboard field', function () { + sharp()->config()->declareEntity(DashboardEntity::class); + + fakeShowFor(PersonEntity::class, new class() extends PersonShow + { + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField( + SharpShowDashboardField::make(DashboardEntity::class) + ); + } + + public function find($id): array + { + return ['name' => 'John Doe', 'age' => 31]; + } + }); + + fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard + { + protected function buildWidgetsData(): void + { + $this->setPanelData('panel', ['name' => 'Albert Einstein']); + } + }); + + $this->sharpShow(PersonEntity::class, 1) + ->sharpDashboardField(DashboardEntity::class) + ->get() + ->assertOk(); +}); From 55c5c787fd5334c6149ab5e3619edf51759df559 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 16 Jan 2026 16:38:36 +0100 Subject: [PATCH 10/12] wip --- docs/.vitepress/sidebar.ts | 3 +- ...esting-with-sharp.md => testing-legacy.md} | 6 +- docs/guide/testing.md | 240 ++++++++++++++++++ docs/guide/upgrading/9.0.md | 2 +- tests/Http/SharpAssertionsHttpTest.php | 2 +- 5 files changed, 249 insertions(+), 4 deletions(-) rename docs/guide/{testing-with-sharp.md => testing-legacy.md} (97%) create mode 100644 docs/guide/testing.md diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index 855dfc670..e3175e4f6 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -101,7 +101,8 @@ export function sidebar(): DefaultTheme.SidebarItem[] { { text: 'Sharp Context', link: '/guide/context.md' }, { text: 'Sharp built-in solution for uploads', link: '/guide/sharp-uploads.md' }, { text: 'Data localization in Form and Show Page', link: '/guide/data-localization.md' }, - { text: 'Testing with Sharp', link: '/guide/testing-with-sharp.md' }, + { text: 'Testing', link: '/guide/testing.md' }, + { text: 'Testing (legacy API)', link: '/guide/testing-legacy.md' }, { text: 'Artisan Generators', link: '/guide/artisan-generators.md' }, { text: 'Style & Visual Theme', link: '/guide/style-visual-theme.md' } ] diff --git a/docs/guide/testing-with-sharp.md b/docs/guide/testing-legacy.md similarity index 97% rename from docs/guide/testing-with-sharp.md rename to docs/guide/testing-legacy.md index 330826f28..c21095edf 100644 --- a/docs/guide/testing-with-sharp.md +++ b/docs/guide/testing-legacy.md @@ -1,4 +1,8 @@ -# Testing with Sharp +# Testing with Sharp (legacy API) + +::: warning +This page documents the old Testing API, we recommend using the new [Testing API](/guide/testing). +::: Sharp provides a few assertions and helpers to help you test your Sharp code. diff --git a/docs/guide/testing.md b/docs/guide/testing.md new file mode 100644 index 000000000..209c6c140 --- /dev/null +++ b/docs/guide/testing.md @@ -0,0 +1,240 @@ +# Testing + +Sharp provides a fluent testing API to help you test your Sharp code. These assertions and helpers are designed to be used in Feature tests. + +## The `SharpAssertions` trait + +To use Sharp's testing helpers, include the `Code16\Sharp\Utils\Testing\SharpAssertions` trait in your test class: + +```php +use Code16\Sharp\Utils\Testing\SharpAssertions; + +class PostFormTest extends TestCase +{ + use SharpAssertions; + + // ... +} +``` + +## Authentication + +### `loginAsSharpUser($user)` + +Sharp provides a helper to log in a user. By default, it will use the `SharpAssertions` internal logic to ensure the user is authorized to access Sharp. + +```php +it('allows the user to access the list', function () { + $user = User::factory()->create(); + + $this + ->loginAsSharpUser($user) + ->sharpList(Post::class) + ->get() + ->assertOk(); +}); +``` + +## Testing Entity Lists + +Use `sharpList()` to test your Entity Lists. + +### `sharpList(string $entityKey)` + +Starts a fluent interaction with an Entity List. + +```php +$this->sharpList(Post::class) + ->get() + ->assertOk() + ->assertListCount(3) + ->assertListContains(['title' => 'My first post']); +``` + +### Filtering the list + +You can use `withFilter()` to apply filters to the list before calling `get()` or a command. + +```php +$this->sharpList(Post::class) + ->withFilter('category', 1) + ->get() + ->assertOk(); +``` + +### Entity Commands + +You can call an Entity Command directly from the list: + +```php +$this->sharpList(Post::class) + ->callEntityCommand(ExportPosts::class, ['format' => 'csv']) + ->assertOk() + ->assertReturnsDownload('posts.csv'); +``` + +### Instance Commands + +Similarly, you can call an Instance Command: + +```php +$this->sharpList(Post::class) + ->callInstanceCommand(1, PublishPost::class) + ->assertOk() + ->assertReturnsReload(); +``` + +### Multi-step Commands (Wizards) + +For commands that have multiple steps, you can use `callNextStep()`: + +```php +$this->sharpList(Post::class) + ->callEntityCommand(MyWizardCommand::class, ['step1_data' => 'value']) + ->assertReturnsStep('step2') + ->callNextStep(['step2_data' => 'value']) + ->assertOk(); +``` + +## Testing Show Pages + +Use `sharpShow()` to test your Show Pages. + +### `sharpShow(string $entityKey, $instanceId)` + +Starts a fluent interaction with a Show Page. + +```php +$this->sharpShow(Post::class, 1) + ->get() + ->assertOk() + ->assertShowData([ + 'title' => 'My first post', + 'author' => 'John Doe' + ]); +``` + +### Instance Commands from Show + +```php +$this->sharpShow(Post::class, 1) + ->callInstanceCommand(PublishPost::class) + ->assertOk(); +``` + +## Testing Forms + +Use `sharpForm()` to test your Forms. + +### `sharpForm(string $entityKey, $instanceId = null)` + +Starts a fluent interaction with a Form. If `$instanceId` is provided, it targets an edit form; otherwise, it targets a creation form. + +### Creating and Updating + +```php +// Create +$this->sharpForm(Post::class) + ->store(['title' => 'New Post']) + ->assertValid() + ->assertRedirect(); + +// Update +$this->sharpForm(Post::class, 1) + ->update(['title' => 'Updated Post']) + ->assertValid() + ->assertRedirect(); +``` + +### Testing the "Creation" or "Edit" request itself + +If you want to test that the form displays correctly: + +```php +$this->sharpForm(Post::class, 1) + ->edit() + ->assertOk() + ->assertFormData(['title' => 'Existing Post']); +``` + +From an `AssertableForm` (the result of `edit()` or `create()`), you can also call `update()` or `store()`: + +```php +$this->sharpForm(Post::class, 1) + ->edit() + ->update(['title' => 'New title']) + ->assertValid(); +``` + +## Testing Dashboards + +Use `sharpDashboard()` to test your Dashboards. + +### `sharpDashboard(string $entityKey)` + +Starts a fluent interaction with a Dashboard. + +```php +$this->sharpDashboard(MyDashboard::class) + ->get() + ->assertOk(); +``` + +### Filtering the dashboard + +```php +$this->sharpDashboard(MyDashboard::class) + ->withFilter('period', '2023') + ->get() + ->assertOk(); +``` + +### Dashboard Commands + +```php +$this->sharpDashboard(MyDashboard::class) + ->callDashboardCommand(RefreshStats::class) + ->assertOk(); +``` + +## Testing Embedded Components + +Show Pages can contain embedded Entity Lists or Dashboards. You can test them using `sharpListField()` and `sharpDashboardField()`. + +### `sharpListField(string $entityKey)` + +```php +$this->sharpShow(Post::class, 1) + ->sharpListField(Comment::class) + ->get() + ->assertOk() + ->assertListCount(5); +``` + +### `sharpDashboardField(string $entityKey)` + +```php +$this->sharpShow(User::class, 1) + ->sharpDashboardField(UserStatsDashboard::class) + ->get() + ->assertOk(); +``` + +## Advanced: Breadcrumbs and Context + +Most of the time, Sharp handles the breadcrumb automatically. However, you might need to simulate a specific breadcrumb context. + +### `withSharpBreadcrumb(Closure $callback)` + +```php +use Code16\Sharp\Utils\Links\BreadcrumbBuilder; + +$this->withSharpBreadcrumb(function (BreadcrumbBuilder $builder) { + return $builder + ->appendEntityList(Category::class) + ->appendShowPage(Category::class, 1); +}) + ->sharpShow(Post::class, 1) + ->get() + ->assertOk(); +``` diff --git a/docs/guide/upgrading/9.0.md b/docs/guide/upgrading/9.0.md index 96447aa82..73b7e3fc3 100644 --- a/docs/guide/upgrading/9.0.md +++ b/docs/guide/upgrading/9.0.md @@ -404,7 +404,7 @@ All assertions, like for instance `assertSharpHasAuthorization`, were removed be This means you also need to remove all `$this->initSharpAssertions()` calls from your tests. -Of course, the test helpers remain available, see the dedicated [testing documentation](../testing-with-sharp.md). +Of course, the test helpers remain available, see the dedicated [testing documentation](../testing.md). Also take note that the `withSharpCurrentBreadcrumb()` method is now deprecated, in favor of the new `withSharpBreadcrumb()` method also documented in the section linked above. diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index bdd35cd11..7a8ab4b34 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -113,7 +113,7 @@ public function label(): ?string public function execute(array $data = []): array { - expect($this->queryParams->filterFor('is_valid'))->toBeTrue(); + // expect($this->queryParams->filterFor('is_valid'))->toBeTrue(); return $this->info('ok'); } From d2febe09efdefe34d0da6fcfb220b6f252fa1860 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 16 Jan 2026 18:43:28 +0100 Subject: [PATCH 11/12] wip --- docs/guide/testing.md | 64 +++--- .../Traits/HandleEntityCommands.php | 3 - .../Commands/AssertableCommandForm.php | 2 - .../Testing/Dashboard/PendingDashboard.php | 4 +- tests/Fixtures/Entities/DashboardEntity.php | 2 +- tests/Fixtures/Entities/PersonEntity.php | 6 +- .../Http/Api/ApiEntityListControllerTest.php | 55 ++--- tests/Http/SharpAssertionsHttpTest.php | 199 ++++++++++++++---- tests/Http/ShowControllerTest.php | 14 +- 9 files changed, 225 insertions(+), 124 deletions(-) diff --git a/docs/guide/testing.md b/docs/guide/testing.md index 209c6c140..b66cb2196 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -4,12 +4,12 @@ Sharp provides a fluent testing API to help you test your Sharp code. These asse ## The `SharpAssertions` trait -To use Sharp's testing helpers, include the `Code16\Sharp\Utils\Testing\SharpAssertions` trait in your test class: +To use Sharp's testing helpers, include the `Code16\Sharp\Utils\Testing\SharpAssertions` trait in your TestCase class: ```php use Code16\Sharp\Utils\Testing\SharpAssertions; -class PostFormTest extends TestCase +abstract class TestCase extends BaseTestCase { use SharpAssertions; @@ -17,6 +17,16 @@ class PostFormTest extends TestCase } ``` +or in `Pest.php`: + +```php +use Code16\Sharp\Utils\Testing\SharpAssertions; + +pest() + ->extend(\Tests\TestCase::class) + ->use(SharpAssertions::class) +``` + ## Authentication ### `loginAsSharpUser($user)` @@ -68,31 +78,46 @@ You can call an Entity Command directly from the list: ```php $this->sharpList(Post::class) - ->callEntityCommand(ExportPosts::class, ['format' => 'csv']) + ->entityCommand(ExportPosts::class) + ->post() ->assertOk() ->assertReturnsDownload('posts.csv'); ``` +If the command has a form, you can test it: + +```php +$this->sharpList(Post::class) + ->entityCommand(ExportPosts::class) + ->getForm() + ->post(['format' => 'csv']) + ->assertOk(); +``` + ### Instance Commands Similarly, you can call an Instance Command: ```php $this->sharpList(Post::class) - ->callInstanceCommand(1, PublishPost::class) + ->instanceCommand(PublishPost::class, 1) + ->post() ->assertOk() ->assertReturnsReload(); ``` ### Multi-step Commands (Wizards) -For commands that have multiple steps, you can use `callNextStep()`: +For commands that have multiple steps, you can use `getNextStepForm()`: ```php $this->sharpList(Post::class) - ->callEntityCommand(MyWizardCommand::class, ['step1_data' => 'value']) + ->entityCommand(MyWizardCommand::class) + ->getForm() + ->post(['step1_data' => 'value']) ->assertReturnsStep('step2') - ->callNextStep(['step2_data' => 'value']) + ->getNextStepForm() + ->post(['step2_data' => 'value']) ->assertOk(); ``` @@ -118,7 +143,8 @@ $this->sharpShow(Post::class, 1) ```php $this->sharpShow(Post::class, 1) - ->callInstanceCommand(PublishPost::class) + ->instanceCommand(PublishPost::class) + ->post() ->assertOk(); ``` @@ -193,7 +219,8 @@ $this->sharpDashboard(MyDashboard::class) ```php $this->sharpDashboard(MyDashboard::class) - ->callDashboardCommand(RefreshStats::class) + ->dashboardCommand(RefreshStats::class) + ->post() ->assertOk(); ``` @@ -219,22 +246,3 @@ $this->sharpShow(User::class, 1) ->get() ->assertOk(); ``` - -## Advanced: Breadcrumbs and Context - -Most of the time, Sharp handles the breadcrumb automatically. However, you might need to simulate a specific breadcrumb context. - -### `withSharpBreadcrumb(Closure $callback)` - -```php -use Code16\Sharp\Utils\Links\BreadcrumbBuilder; - -$this->withSharpBreadcrumb(function (BreadcrumbBuilder $builder) { - return $builder - ->appendEntityList(Category::class) - ->appendShowPage(Category::class, 1); -}) - ->sharpShow(Post::class, 1) - ->get() - ->assertOk(); -``` diff --git a/src/EntityList/Traits/HandleEntityCommands.php b/src/EntityList/Traits/HandleEntityCommands.php index c2bd7f9cf..87a56e91a 100644 --- a/src/EntityList/Traits/HandleEntityCommands.php +++ b/src/EntityList/Traits/HandleEntityCommands.php @@ -45,8 +45,6 @@ protected function appendEntityCommandsToConfig(array &$config): void final public function getEntityCommandsHandlers(): Collection { - ray($this->entityCommandHandlers); - ray()->trace(); if ($this->entityCommandHandlers === null) { $groupIndex = 0; $this->entityCommandHandlers = collect($this->getEntityCommands()) @@ -75,7 +73,6 @@ final public function getEntityCommandsHandlers(): Collection $commandHandler->setCommandKey($commandKey); } - // ray($this->queryParams ?? null, $commandHandler)->trace(); if (isset($this->queryParams)) { // We have to init query params of the command $commandHandler->initQueryParams($this->queryParams); diff --git a/src/Utils/Testing/Commands/AssertableCommandForm.php b/src/Utils/Testing/Commands/AssertableCommandForm.php index df134e8a6..2ea3c8b27 100644 --- a/src/Utils/Testing/Commands/AssertableCommandForm.php +++ b/src/Utils/Testing/Commands/AssertableCommandForm.php @@ -26,8 +26,6 @@ public function __construct( public function post(array $data = []): AssertableCommand { - ray($this->formData()); - return new AssertableCommand( postCommand: fn ($data, $step) => ($this->post)($data, $step, $this->formData()), getForm: $this->getForm, diff --git a/src/Utils/Testing/Dashboard/PendingDashboard.php b/src/Utils/Testing/Dashboard/PendingDashboard.php index fe52f1d58..aced2f3d3 100644 --- a/src/Utils/Testing/Dashboard/PendingDashboard.php +++ b/src/Utils/Testing/Dashboard/PendingDashboard.php @@ -82,7 +82,7 @@ public function dashboardCommand(string $commandKeyOrClassName): PendingCommand route( 'code16.sharp.api.dashboard.command.form', [ - 'entityKey' => $this->entityKey, + 'dashboardKey' => $this->entityKey, 'commandKey' => $commandKey, 'command_step' => $step, ...$this->dashboardQueryParams(), @@ -97,7 +97,7 @@ public function dashboardCommand(string $commandKeyOrClassName): PendingCommand ->postJson( route( 'code16.sharp.api.dashboard.command', - ['entityKey' => $this->entityKey, 'commandKey' => $commandKey] + ['dashboardKey' => $this->entityKey, 'commandKey' => $commandKey] ), [ 'data' => $this->formatDataForCommand( diff --git a/tests/Fixtures/Entities/DashboardEntity.php b/tests/Fixtures/Entities/DashboardEntity.php index 18e38a5ab..c18004f20 100644 --- a/tests/Fixtures/Entities/DashboardEntity.php +++ b/tests/Fixtures/Entities/DashboardEntity.php @@ -23,7 +23,7 @@ public function setShow(?SharpDashboard $show): self protected function getView(): SharpDashboard { - return $this->fakeView ?? parent::getView(); + return isset($this->fakeView) ? clone $this->fakeView : parent::getView(); } public function setPolicy(SharpEntityPolicy $policy): self diff --git a/tests/Fixtures/Entities/PersonEntity.php b/tests/Fixtures/Entities/PersonEntity.php index 15e1e402f..41b78a3f5 100644 --- a/tests/Fixtures/Entities/PersonEntity.php +++ b/tests/Fixtures/Entities/PersonEntity.php @@ -57,17 +57,17 @@ public function setForm(?SharpForm $form): self public function getForm(): ?SharpForm { - return $this->fakeForm ?? parent::getForm(); + return isset($this->fakeForm) ? clone $this->fakeForm : parent::getForm(); } protected function getShow(): ?SharpShow { - return $this->fakeShow ?? parent::getShow(); + return isset($this->fakeShow) ? clone $this->fakeShow : parent::getShow(); } protected function getList(): ?SharpEntityList { - return $this->fakeList ?? parent::getList(); + return isset($this->fakeList) ? clone $this->fakeList : parent::getList(); } public function setValidator(string $validatorClass, ?string $subentity = null): self diff --git a/tests/Http/Api/ApiEntityListControllerTest.php b/tests/Http/Api/ApiEntityListControllerTest.php index 644bb4a99..64889b187 100644 --- a/tests/Http/Api/ApiEntityListControllerTest.php +++ b/tests/Http/Api/ApiEntityListControllerTest.php @@ -20,29 +20,27 @@ }); it('allows to reorder instances', function () { - $this->withoutExceptionHandling(); + $ids = null; - $list = new class() extends PersonList + fakeListFor('person', new class($ids) extends PersonList { - public array $reorderedInstances = []; + public function __construct(public &$ids) {} public function buildListConfig(): void { $this->configureReorderable( - new class($this->reorderedInstances) implements ReorderHandler + new class($this->ids) implements ReorderHandler { - public function __construct(public array &$reorderedInstances) {} + public function __construct(public &$ids) {} public function reorder(array $ids): void { - $this->reorderedInstances = $ids; + $this->ids = $ids; } } ); } - }; - - fakeListFor('person', $list); + }); $this ->postJson( @@ -51,52 +49,45 @@ public function reorder(array $ids): void ) ->assertOk(); - expect($list->reorderedInstances)->toEqual([3, 2, 1]); + expect($ids)->toEqual([3, 2, 1]); }); it('allows to delete an instance in the entity list if delete method is implemented', function () { - $list = new class() extends PersonList + $deletedId = null; + + fakeListFor('person', new class($deletedId) extends PersonList { - public ?int $deletedInstance = null; + public function __construct(public &$deletedId) {} public function delete($id): void { - $this->deletedInstance = $id; + $this->deletedId = $id; } - }; - - fakeListFor('person', $list); - - $idToDelete = rand(1, 10); + }); - $this->deleteJson(route('code16.sharp.api.list.delete', ['root', 'person', $idToDelete])) + $this->deleteJson(route('code16.sharp.api.list.delete', ['root', 'person', 1])) ->assertOk(); - expect($list->deletedInstance)->toEqual($idToDelete); + expect($deletedId)->toEqual(1); }); it('delegates deletion to the show page if it exists', function () { - $this->withoutExceptionHandling(); - - fakeListFor('person', new PersonList()); + $deletedId = null; - $show = new class() extends PersonShow + fakeShowFor('person', new class($deletedId) extends PersonShow { - public ?int $deletedInstance = null; + public function __construct(public &$deletedId) {} public function delete($id): void { - $this->deletedInstance = $id; + $this->deletedId = $id; } - }; - fakeShowFor('person', $show); - - $idToDelete = rand(1, 10); + }); - $this->deleteJson(route('code16.sharp.api.list.delete', ['root', 'person', $idToDelete])) + $this->deleteJson(route('code16.sharp.api.list.delete', ['root', 'person', 1])) ->assertOk(); - expect($show->deletedInstance)->toEqual($idToDelete); + expect($deletedId)->toEqual(1); }); it('checks if the entity list allows deletion', function () { diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index 7a8ab4b34..bc20e1a84 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -1,8 +1,10 @@ config()->declareEntity(PersonEntity::class); sharp()->config()->declareEntity(SinglePersonEntity::class); + sharp()->config()->declareEntity(DashboardEntity::class); }); it('get & assert an entity list', function () { - fakeListFor(PersonEntity::class, new class() extends PersonList + $filterValues = []; + + fakeListFor(PersonEntity::class, new class($filterValues) extends PersonList { + public function __construct(public &$filterValues) {} + + protected function getFilters(): ?array + { + return [ + new class() extends CheckFilter + { + public function buildFilterConfig(): void + { + $this->configureKey('is_valid'); + } + }, + ]; + } + public function getListData(): array { + $this->filterValues = ['is_valid' => $this->queryParams->filterFor('is_valid')]; + return [ ['id' => 1, 'name' => 'Marie Curie'], ]; @@ -39,33 +61,29 @@ public function getListData(): array }); $this->sharpList(PersonEntity::class) + ->withFilter('is_valid', true) ->get() ->assertOk() ->assertListCount(1) ->assertListContains(['name' => 'Marie Curie']); + + expect($filterValues)->toEqual(['is_valid' => true]); }); -it('call & assert an entity list entity command', function () { - fakeListFor(PersonEntity::class, new class() extends PersonList +it('call & assert an entity list entity command form', function () { + $postedData = []; + + fakeListFor(PersonEntity::class, new class($postedData) extends PersonList { - protected function getFilters(): ?array - { - return [ - new class() extends \Code16\Sharp\Filters\CheckFilter - { - public function buildFilterConfig(): void - { - $this->configureKey('is_valid'); - } - }, - ]; - } + public function __construct(public array &$postedData) {} protected function getEntityCommands(): ?array { return [ - 'cmd-form' => new class() extends EntityCommand + 'cmd-form' => new class($this->postedData) extends EntityCommand { + public function __construct(public array &$postedData) {} + public function label(): ?string { return 'entity'; @@ -85,7 +103,7 @@ protected function initialData(): array public function execute(array $data = []): array { - expect($data)->toMatchArray(['field_with_initial_value' => 'test']); + $this->postedData = $data; if ($data['action'] === 'download') { Storage::fake('files'); @@ -104,35 +122,16 @@ public function execute(array $data = []): array }; } }, - 'cmd' => new class() extends EntityCommand - { - public function label(): ?string - { - return 'entity'; - } - - public function execute(array $data = []): array - { - // expect($this->queryParams->filterFor('is_valid'))->toBeTrue(); - - return $this->info('ok'); - } - }, ]; } }); - $this->withoutExceptionHandling(); - - $this->sharpList(PersonEntity::class) - ->withFilter('is_valid', true) - ->entityCommand('cmd')->post() - ->assertReturnsInfo('ok'); - $this->sharpList(PersonEntity::class) ->entityCommand('cmd-form')->getForm()->post(['action' => 'info']) ->assertReturnsInfo('ok'); + expect($postedData)->toEqual(['action' => 'info', 'field_with_initial_value' => 'test']); + $this->sharpList(PersonEntity::class) ->entityCommand('cmd-form')->getForm()->post(['action' => 'link']) ->assertReturnsLink('https://example.org'); @@ -156,14 +155,73 @@ public function execute(array $data = []): array ->assertReturnsRefresh([1, 2]); }); +it('call & assert an entity list entity command with filters', function () { + $filterValues = []; + + fakeListFor(PersonEntity::class, new class($filterValues) extends PersonList + { + public function __construct(public &$filterValues) {} + + protected function getFilters(): ?array + { + return [ + new class() extends CheckFilter + { + public function buildFilterConfig(): void + { + $this->configureKey('is_valid'); + } + }, + ]; + } + + protected function getEntityCommands(): ?array + { + return [ + 'cmd' => new class($this->filterValues) extends EntityCommand + { + public function __construct(public &$filterValues) {} + + public function label(): ?string + { + return 'entity'; + } + + public function execute(array $data = []): array + { + $this->filterValues = ['is_valid' => $this->queryParams->filterFor('is_valid')]; + + return $this->info('ok'); + } + }, + ]; + } + }); + + $this->withoutExceptionHandling(); + + $this->sharpList(PersonEntity::class) + ->withFilter('is_valid', true) + ->entityCommand('cmd')->post() + ->assertReturnsInfo('ok'); + + expect($filterValues)->toEqual(['is_valid' => true]); +}); + it('call & assert an entity list entity wizard command', function () { - fakeListFor(PersonEntity::class, new class() extends PersonList + $postedData = []; + + fakeListFor(PersonEntity::class, new class($postedData) extends PersonList { + public function __construct(public &$postedData) {} + protected function getEntityCommands(): ?array { return [ - 'wizard' => new class() extends EntityWizardCommand + 'wizard' => new class($this->postedData) extends EntityWizardCommand { + public function __construct(public &$postedData) {} + public function label(): ?string { return 'my command'; @@ -183,7 +241,7 @@ public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void protected function executeFirstStep(array $data): array { - expect($data)->toEqual(['name' => 'John', 'field_with_initial_value' => 'test']); + $this->postedData = $data; $this->validate($data, ['name' => 'required']); @@ -204,7 +262,7 @@ public function buildFormFieldsForStepSecondStep(FieldsContainer $formFields): v protected function executeStepSecondStep(array $data): array { - expect($data)->toEqual(['field_with_initial_value' => 'test', 'age' => 30]); + $this->postedData = $data; return $this->reload(); } @@ -219,8 +277,10 @@ protected function executeStepSecondStep(array $data): array ->entityCommand('wizard') ->getForm()->post(['name' => 'John']) ->assertReturnsStep('second-step') + ->tap(fn () => expect($postedData)->toEqual(['name' => 'John', 'field_with_initial_value' => 'test'])) ->getNextStepForm()->post(['age' => 30]) - ->assertReturnsReload(); + ->assertReturnsReload() + ->tap(fn () => expect($postedData)->toEqual(['age' => 30, 'field_with_initial_value' => 'test'])); }); it('call & assert a entity list instance command', function () { @@ -562,9 +622,6 @@ public function update($id, array $data) }); test('get dashboard', function () { - - sharp()->config()->declareEntity(DashboardEntity::class); - fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard { protected function buildWidgetsData(): void @@ -609,3 +666,53 @@ protected function buildWidgetsData(): void ->get() ->assertOk(); }); + +it('call & assert a dashboard command', function () { + fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard + { + public function getDashboardCommands(): ?array + { + return [ + 'cmd-form' => new class() extends DashboardCommand + { + public function label(): ?string + { + return 'dashboard'; + } + + public function buildFormFields(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('action')); + } + + public function execute(array $data = []): array + { + return match ($data['action']) { + 'info' => $this->info('dashboard'), + }; + } + }, + 'cmd' => new class() extends DashboardCommand + { + public function label(): ?string + { + return 'dashboard'; + } + + public function execute(array $data = []): array + { + return $this->info('dashboard'); + } + }, + ]; + } + }); + + $this->sharpDashboard(DashboardEntity::class) + ->dashboardCommand('cmd')->post() + ->assertReturnsInfo('dashboard'); + + $this->sharpDashboard(DashboardEntity::class) + ->dashboardCommand('cmd-form')->getForm()->post(['action' => 'info']) + ->assertReturnsInfo('dashboard'); +}); diff --git a/tests/Http/ShowControllerTest.php b/tests/Http/ShowControllerTest.php index fb89557cf..f87ab0841 100644 --- a/tests/Http/ShowControllerTest.php +++ b/tests/Http/ShowControllerTest.php @@ -219,22 +219,22 @@ public function findSingle(): array }); it('allows instance deletion from the show', function () { - $personShow = new class() extends PersonShow + $deletedId = null; + + fakeShowFor('person', new class($deletedId) extends PersonShow { - public bool $wasDeleted = false; + public function __construct(public &$deletedId) {} public function delete($id): void { - $this->wasDeleted = true; + $this->deletedId = $id; } - }; - - fakeShowFor('person', $personShow); + }); $this->delete('/sharp/root/s-list/person/s-show/person/1') ->assertRedirect('/sharp/root/s-list/person'); - expect($personShow->wasDeleted)->toBeTrue(); + expect($deletedId)->toEqual(1); }); it('disallows instance deletion without authorization', function () { From 7227768fefbdc55cf6530ff5685915d76142cc59 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 16 Jan 2026 18:58:36 +0100 Subject: [PATCH 12/12] update test + improve doc --- docs/guide/testing.md | 4 ++-- tests/Http/SharpAssertionsHttpTest.php | 32 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/guide/testing.md b/docs/guide/testing.md index b66cb2196..83bc4541e 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -67,7 +67,7 @@ You can use `withFilter()` to apply filters to the list before calling `get()` o ```php $this->sharpList(Post::class) - ->withFilter('category', 1) + ->withFilter(CategoryFilter::class, 1) ->get() ->assertOk(); ``` @@ -210,7 +210,7 @@ $this->sharpDashboard(MyDashboard::class) ```php $this->sharpDashboard(MyDashboard::class) - ->withFilter('period', '2023') + ->withFilter(PeriodFilter::class, ['start' => '2023-01-01', 'end' => '2023-01-31']) ->get() ->assertOk(); ``` diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php index bc20e1a84..ace547b3a 100644 --- a/tests/Http/SharpAssertionsHttpTest.php +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -5,6 +5,8 @@ use Code16\Sharp\EntityList\Commands\InstanceCommand; use Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand; use Code16\Sharp\Filters\CheckFilter; +use Code16\Sharp\Filters\DateRange\DateRangeFilterValue; +use Code16\Sharp\Filters\DateRangeFilter; use Code16\Sharp\Form\Fields\SharpFormEditorField; use Code16\Sharp\Form\Fields\SharpFormTextField; use Code16\Sharp\Show\Fields\SharpShowDashboardField; @@ -622,17 +624,45 @@ public function update($id, array $data) }); test('get dashboard', function () { - fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard + /** @var array{'period':DateRangeFilterValue} $filterValues */ + $filterValues = []; + + fakeDashboardFor(DashboardEntity::class, new class($filterValues) extends TestDashboard { + public function __construct(public &$filterValues) {} + + public function getFilters(): ?array + { + return [ + new class() extends DateRangeFilter + { + public function label(): string + { + return 'Period'; + } + + public function buildFilterConfig(): void + { + $this->configureKey('period'); + } + }, + ]; + } + protected function buildWidgetsData(): void { + $this->filterValues = ['period' => $this->queryParams->filterFor('period')]; $this->setPanelData('panel', ['name' => 'Marie Curie']); } }); $this->sharpDashboard(DashboardEntity::class) + ->withFilter('period', ['start' => '2021-01-01', 'end' => '2021-01-31']) ->get() ->assertOk(); + + expect($filterValues['period']->getStart()->format('Y-m-d'))->toEqual('2021-01-01') + ->and($filterValues['period']->getEnd()->format('Y-m-d'))->toEqual('2021-01-31'); }); test('get show dashboard field', function () {