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/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/Commands/AssertableCommand.php b/src/Utils/Testing/Commands/AssertableCommand.php new file mode 100644 index 000000000..4dc7873f0 --- /dev/null +++ b/src/Utils/Testing/Commands/AssertableCommand.php @@ -0,0 +1,170 @@ +response = $this->post(); + } + + public function assertViewHas(mixed $key, mixed $value = null): static + { + $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') + ->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('link', $url) + ->etc() + ); + + return $this; + } + + public function assertReturnsReload(): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'reload') + ->etc() + ); + + return $this; + } + + public function assertReturnsRefresh(array $ids): static + { + $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 = null): static + { + $this->response->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'step') + ->etc() + ); + + 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/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 new file mode 100644 index 000000000..e703cb02c --- /dev/null +++ b/src/Utils/Testing/EntityList/AssertableEntityList.php @@ -0,0 +1,42 @@ +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 new file mode 100644 index 000000000..5e54bef5c --- /dev/null +++ b/src/Utils/Testing/EntityList/PendingEntityList.php @@ -0,0 +1,141 @@ +entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); + $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->parent instanceof PendingShow ? $this->parent : $this + ); + } + + 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->entityListQueryParams(), + ]) + ) + ); + } + + public function callEntityCommand( + 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->getCurrentPageUrlFromParents(), + ) + ->postJson( + route( + 'code16.sharp.api.list.command.entity', + ['entityKey' => $this->entityKey, 'commandKey' => $commandKey] + ), + [ + 'data' => $data, + 'query' => $this->entityListQueryParams(), + 'command_step' => $step, + ], + ), + commandContainer: $this->entityList, + data: $data, + step: $commandStep + ); + } + + public function callInstanceCommand( + int|string $instanceId, + 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->getCurrentPageUrlFromParents(), + ) + ->postJson( + route( + 'code16.sharp.api.list.command.instance', + ['entityKey' => $this->entityKey, 'instanceId' => $instanceId, 'commandKey' => $commandKey] + ), + [ + 'data' => $data, + 'query' => $this->entityListQueryParams(), + 'command_step' => $step, + ], + ), + commandContainer: $this->entityList, + data: $data, + step: $commandStep, + ); + } + + protected function entityListQueryParams(): array + { + return $this->entityList + ->filterContainer() + ->getQueryParamsFromFilterValues($this->filterValues) + ->all(); + } +} 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/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 1e1bf980b..d97175721 100644 --- a/src/Utils/Testing/SharpAssertions.php +++ b/src/Utils/Testing/SharpAssertions.php @@ -3,16 +3,34 @@ 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; +use Code16\Sharp\Utils\Testing\Form\PendingForm; +use Code16\Sharp\Utils\Testing\Show\PendingShow; trait SharpAssertions { + use GeneratesCurrentPageUrl; + use GeneratesGlobalFilterUrl; + private BreadcrumbBuilder $breadcrumbBuilder; - private ?string $globalFilter = null; + + 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 @@ -43,17 +61,9 @@ public function withSharpBreadcrumb(Closure $callback): self 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 +82,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 +104,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 +133,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 +152,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 +172,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 +191,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 +211,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 +236,7 @@ public function callSharpInstanceCommandFromList( array $data = [], ?string $commandStep = null ) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -257,7 +267,7 @@ public function callSharpInstanceCommandFromShow( array $data = [], ?string $commandStep = null ) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -287,7 +297,7 @@ public function callSharpEntityCommandFromList( array $data = [], ?string $commandStep = null ) { - URL::defaults(['globalFilter' => $this->globalFilter ?: GlobalFilters::$defaultKey]); + $this->setGlobalFilterUrlDefault(); $entityKey = $this->resolveEntityKey($entityClassNameOrKey); @@ -322,18 +332,6 @@ private function breadcrumbBuilder(string $entityKey, ?string $instanceId = null ->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/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 new file mode 100644 index 000000000..596547a50 --- /dev/null +++ b/src/Utils/Testing/Show/PendingShow.php @@ -0,0 +1,98 @@ +entityKey = app(SharpEntityManager::class)->entityKeyFor($entityKey); + $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 = [], + ?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->getCurrentPageUrlFromParents(), + ) + ->postJson( + route( + 'code16.sharp.api.show.command.instance', + ['entityKey' => $this->entityKey, 'instanceId' => $this->instanceId, 'commandKey' => $commandKey] + ), + [ + 'data' => $data, + 'command_step' => $step, + ], + ), + commandContainer: $this->show, + data: $data, + step: $commandStep, + ); + } +} diff --git a/tests/Http/SharpAssertionsHttpTest.php b/tests/Http/SharpAssertionsHttpTest.php new file mode 100644 index 000000000..e2a6c7e6b --- /dev/null +++ b/tests/Http/SharpAssertionsHttpTest.php @@ -0,0 +1,190 @@ +use(SharpAssertions::class); + +beforeEach(function () { + login(); + sharp()->config()->declareEntity(PersonEntity::class); +}); + +it('get & assert 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() + // ->getListData() + ->assertOk() + ->assertListCount(1) + ->assertListContains(['name' => 'Marie Curie']); +}); + +it('call & assert 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 + { + 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]), + }; + } + }, + ]; + } + }); + + $this->sharpList(PersonEntity::class) + ->callEntityCommand('cmd', ['action' => 'info']) + ->assertReturnsInfo('ok'); + + $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(); +}); + +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(); +}); diff --git a/tests/Unit/Utils/Testing/SharpAssertionsTest.php b/tests/Unit/Utils/Testing/SharpAssertionsTest.php index 64b2fc166..d75e7ee5a 100644 --- a/tests/Unit/Utils/Testing/SharpAssertionsTest.php +++ b/tests/Unit/Utils/Testing/SharpAssertionsTest.php @@ -1,271 +1,108 @@ getSharpShow('leaves', 6); +beforeEach(function () { + sharp()->config()->declareEntity(PersonEntity::class); - $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], + 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, + ] ) - ->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], + ->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, + ] ) - ->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'); + ->andReturn(new TestResponse(new Response())); - $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; - - 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; - } - }; -} 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; + } + }; +}