diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f064804..ca52647 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,12 +9,10 @@ jobs: fail-fast: true matrix: os: [ ubuntu-latest ] - php: [ 7.3, 7.4 ] - laravel: [ 7.*, 8.* ] + php: [ 7.3, 7.4, 8.0 ] + laravel: [ 8.* ] stability: [ prefer-lowest, prefer-stable ] include: - - laravel: 7.* - testbench: 5.* - laravel: 8.* testbench: 6.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 85f9b3b..f96ef86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,29 @@ All notable changes to `nova-enum-field` will be documented in this file. +## 2.3.0 - 2021-01-25 + +- Drop support for Laravel `7.x` +- Drop support for `laravel-enum < 3.1`. +- Add a customisable select filter. +- Add a customisable boolean filter. +- Add a flagged enum field. +- Refactor and simplify tests. + +## 2.2.0 - 2020-09-25 + +- Add support for Laravel `8.x`. +- Add support for PHP `8.0`. +- Allow editing enums without casts. + +## 2.1.0 - 2020-09-01 + +- Add support for `laravel-enum 2.2.0`. + ## 2.0.0 - 2020-07-08 -- Update `larave-enum` to `2.x` -- Drop support for laravel < `7.x` +- Add support for `laravel-enum 2.x`. +- Drop support for Laravel `< 7.x`. ## 1.1.0 - 2019-09-30 @@ -16,7 +35,7 @@ All notable changes to `nova-enum-field` will be documented in this file. ## 1.0.4 - 2019-09-27 - Add documentation. -- Refactor field code. +- Refactor `Enum` field code. ## 1.0.3 - 2019-09-27 diff --git a/README.md b/README.md index 0df5c87..ae11f63 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ composer require simplesquid/nova-enum-field ## Setup -This package requires that you use Attribute Casting in your models. From the docs at [BenSampo/laravel-enum](https://github.com/BenSampo/laravel-enum#attribute-casting), this can be done like so: +It is strongly recommended that you use Attribute Casting in your models. From the docs at [BenSampo/laravel-enum](https://github.com/BenSampo/laravel-enum#attribute-casting), this can be done like this: ```php +use App\Enums\UserType; use BenSampo\Enum\Traits\CastsEnums; -use BenSampo\Enum\Tests\Enums\UserType; use Illuminate\Database\Eloquent\Model; class Example extends Model @@ -38,12 +38,12 @@ class Example extends Model ## Usage -You can use the `Enum` field in your Nova resource like so: +You can use the `Enum` field in your Nova resource like this: ```php namespace App\Nova; -use BenSampo\Enum\Tests\Enums\UserType; +use App\Enums\UserType; use SimpleSquid\Nova\Fields\Enum\Enum; class Example extends Resource @@ -55,7 +55,7 @@ class Example extends Resource return [ // ... - Enum::make('User Type')->attachEnum(UserType::class), + Enum::make('User Type')->attach(UserType::class), // ... ]; @@ -63,6 +63,96 @@ class Example extends Resource } ``` +### Flagged Enums + +You can use the `FlaggedEnum` field in your Nova resource like this (see [Flagged/Bitwise Enum](https://github.com/BenSampo/laravel-enum#flaggedbitwise-enum) setup): + +```php +namespace App\Nova; + +use App\Enums\UserPermissions; +use SimpleSquid\Nova\Fields\Enum\FlaggedEnum; + +class Example extends Resource +{ + // ... + + public function fields(Request $request) + { + return [ + // ... + + FlaggedEnum::make('User Permissions')->attach(UserPermissions::class), + + // ... + ]; + } +} +``` + +### Filters + +If you would like to use the provided Nova Select filter (which is compatible with both the `Enum` and `FlaggedEnum` fields), you can include it like this: + +```php +namespace App\Nova; + +use App\Enums\UserPermissions; +use App\Enums\UserType; +use SimpleSquid\Nova\Fields\Enum\EnumFilter; + +class Example extends Resource +{ + // ... + + public function filters(Request $request) + { + return [ + new EnumFilter('user_type', UserType::class), + + new EnumFilter('user_permissions', UserPermissions::class), + + // Or with optional filter name: + (new EnumFilter('user_type', UserType::class)) + ->name('Type of user'), + ]; + } +} +``` + +Alternatively, you may wish to use the provided Nova Boolean filter (which is also compatible with both the `Enum` and `FlaggedEnum` fields): + +```php +namespace App\Nova; + +use App\Enums\UserPermissions; +use App\Enums\UserType; +use SimpleSquid\Nova\Fields\Enum\EnumBooleanFilter; + +class Example extends Resource +{ + // ... + + public function filters(Request $request) + { + return [ + new EnumBooleanFilter('user_type', UserType::class), + + new EnumBooleanFilter('user_permissions', UserPermissions::class), + + // Or with optional filter name: + (new EnumBooleanFilter('user_type', UserType::class)) + ->name('Type of user'), + + // When filtering a FlaggedEnum, it will default to filtering + // by ANY flags, however you may wish to filter by ALL flags: + (new EnumBooleanFilter('user_permissions', UserPermissions::class)) + ->filterAllFlags(), + ]; + } +} +``` + ## Testing ``` bash diff --git a/composer.json b/composer.json index e849199..1968b2f 100644 --- a/composer.json +++ b/composer.json @@ -27,14 +27,16 @@ ], "require": { "php": "^7.3|^8.0", - "bensampo/laravel-enum": "^2.0|^3.0", - "cakephp/chronos": "^1.2.3|^2.0", - "illuminate/support": "^7.0|^8.0", + "bensampo/laravel-enum": "^3.1", + "illuminate/support": "^8.0", "laravel/nova": "^3.0" }, "require-dev": { - "orchestra/testbench": "^5.0|^6.0", - "phpunit/phpunit": "^8.2|^9.0", + "joshgaber/novaunit": "^2.2", + "mockery/mockery": "^1.3.3", + "nunomaduro/collision": "^5.2", + "orchestra/testbench": "^6.0", + "phpunit/phpunit": "^9.3.3", "symfony/var-dumper": "^5.0" }, "autoload": { @@ -47,6 +49,11 @@ "SimpleSquid\\Nova\\Fields\\Enum\\Tests\\": "tests" } }, + "extra": { + "laravel": { + "providers": [] + } + }, "scripts": { "test": "vendor/bin/phpunit --colors=always" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1318ca3..ba6889a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,6 +10,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" + printerClass="NunoMaduro\Collision\Adapters\Phpunit\Printer" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> @@ -18,7 +19,8 @@ - tests + tests/Fields + tests/Filters diff --git a/src/Enum.php b/src/Enum.php index b28f5a8..3c1ff9a 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -1,64 +1,49 @@ options($this->getEnumOptions($enumClass)) - ->rules('required', new EnumValue($enumClass, false)) - ->resolveUsing( - function ($enum) { - return $enum instanceof \BenSampo\Enum\Enum ? $enum->value : $enum; - } - ) - ->displayUsing( - function ($enum) { - return $enum instanceof \BenSampo\Enum\Enum ? $enum->description : $enum; - } - ); + parent::__construct($name, $attribute, $resolveCallback); + + $this->resolveUsing( + function ($value) { + return $value instanceof \BenSampo\Enum\Enum ? $value->value : $value; + } + ); + + $this->displayUsing( + function ($value) { + return $value instanceof \BenSampo\Enum\Enum ? $value->description : $value; + } + ); + + $this->fillUsing( + function (NovaRequest $request, $model, $attribute, $requestAttribute) { + if ($request->exists($requestAttribute)) { + $model->{$attribute} = $request[$requestAttribute]; + } + } + ); } - /** - * Hydrate the given attribute on the model based on the incoming request. - * - * @param NovaRequest $request - * @param string $requestAttribute - * @param object $model - * @param string $attribute - * - * @return void - */ - protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute) + public function attach($class) { - if ($request->exists($requestAttribute)) { - $model->{$attribute} = $request[$requestAttribute]; - } + return $this->options($class::asSelectArray()) + ->rules('required', new EnumValue($class, false)); } - protected function getEnumOptions(string $enumClass): array + /** + * @deprecated deprecated since version 2.3 + */ + public function attachEnum($class) { - // Since laravel-enum v2.2.0, the method has been named 'asSelectArray' - if (in_array(Arrayable::class, class_implements($enumClass))) { - return $enumClass::asSelectArray(); - } - - return $enumClass::toSelectArray(); + return $this->attach($class); } } diff --git a/src/EnumBooleanFilter.php b/src/EnumBooleanFilter.php new file mode 100644 index 0000000..1d7f800 --- /dev/null +++ b/src/EnumBooleanFilter.php @@ -0,0 +1,93 @@ +column = $column; + $this->class = $class; + + $this->flagged = is_subclass_of($this->class, \BenSampo\Enum\FlaggedEnum::class); + } + + public function filterAnyFlags() + { + $this->scope = 'any'; + + return $this; + } + + public function filterAllFlags() + { + $this->scope = 'all'; + + return $this; + } + + public function name($name = null) + { + if (is_null($name)) { + return $this->name ?: Nova::humanize($this->column); + } + + $this->name = $name; + + return $this; + } + + public function apply(Request $request, $query, $value) + { + $enums = array_keys(array_filter($value)); + + if (empty($enums)) { + return $query; + } + + if ($this->flagged && $this->scope === 'all') { + return $this->scopeHasAllFlags($query, $this->column, $enums); + } + + return $query->where( + function ($query) use ($enums) { + if ($this->flagged) { + $query = $this->scopeHasAnyFlags($query, $this->column, $enums); + + $enums = in_array($this->class::None, $enums) ? [$this->class::None] : []; + } else { + $query->where($this->column, array_shift($enums)); + } + + foreach ($enums as $enum) { + $query->orWhere($this->column, $enum); + } + } + ); + } + + public function options(Request $request) + { + if ($this->flagged && $this->scope === 'all') { + return array_flip(Arr::except($this->class::asSelectArray(), $this->class::None)); + } + + return array_flip($this->class::asSelectArray()); + } +} diff --git a/src/EnumFilter.php b/src/EnumFilter.php new file mode 100644 index 0000000..d363104 --- /dev/null +++ b/src/EnumFilter.php @@ -0,0 +1,52 @@ +column = $column; + $this->class = $class; + + $this->flagged = is_subclass_of($this->class, \BenSampo\Enum\FlaggedEnum::class); + } + + public function name($name = null) + { + if (is_null($name)) { + return $this->name ?: Nova::humanize($this->column); + } + + $this->name = $name; + + return $this; + } + + public function apply(Request $request, $query, $value) + { + if ($this->flagged && $value != $this->class::None) { + return $this->scopeHasFlag($query, $this->column, $value); + } + + return $query->where($this->column, $value); + } + + public function options(Request $request) + { + return array_flip($this->class::asSelectArray()); + } +} diff --git a/src/FlaggedEnum.php b/src/FlaggedEnum.php new file mode 100644 index 0000000..fb855a2 --- /dev/null +++ b/src/FlaggedEnum.php @@ -0,0 +1,44 @@ +noValueText('None'); + } + + public function attach($class) + { + $this->resolveUsing( + function ($value) use ($class) { + if (! $value instanceof \BenSampo\Enum\FlaggedEnum) { + return $value; + } + + return collect($value->getValues())->mapWithKeys(function ($flag) use ($value) { + return [$flag => $value->hasFlag($flag)]; + })->except($class::None)->all(); + } + ); + + $this->fillUsing( + function (NovaRequest $request, $model, $attribute, $requestAttribute) use ($class) { + if ($request->exists($requestAttribute)) { + $value = json_decode($request[$requestAttribute], true); + + $model->{$attribute} = $class::flags(array_keys(array_filter($value))); + } + } + ); + + return $this->options(Arr::except($class::asSelectArray(), $class::None)); + } +} diff --git a/tests/Examples/FlaggedEnum.php b/tests/Examples/FlaggedEnum.php new file mode 100644 index 0000000..8bec39e --- /dev/null +++ b/tests/Examples/FlaggedEnum.php @@ -0,0 +1,20 @@ + ExampleIntegerEnum::class, + 'enum' => FlaggedEnum::class, ]; } diff --git a/tests/Examples/ExampleIntegerEnum.php b/tests/Examples/IntegerEnum.php similarity index 87% rename from tests/Examples/ExampleIntegerEnum.php rename to tests/Examples/IntegerEnum.php index beefdf1..4b25f67 100644 --- a/tests/Examples/ExampleIntegerEnum.php +++ b/tests/Examples/IntegerEnum.php @@ -9,7 +9,7 @@ * @method static Moderator() * @method static Subscriber() */ -class ExampleIntegerEnum extends Enum +class IntegerEnum extends Enum { const Administrator = 0; diff --git a/tests/Examples/IntegerModel.php b/tests/Examples/IntegerModel.php new file mode 100644 index 0000000..cc9f928 --- /dev/null +++ b/tests/Examples/IntegerModel.php @@ -0,0 +1,21 @@ + IntegerEnum::class, + ]; +} diff --git a/tests/Examples/NoCastsModel.php b/tests/Examples/NoCastsModel.php new file mode 100644 index 0000000..dcd8100 --- /dev/null +++ b/tests/Examples/NoCastsModel.php @@ -0,0 +1,14 @@ + StringEnum::class, + ]; +} diff --git a/tests/Fields/FieldTest.php b/tests/Fields/FieldTest.php new file mode 100644 index 0000000..8d180fd --- /dev/null +++ b/tests/Fields/FieldTest.php @@ -0,0 +1,59 @@ +setUpDatabase($this->app); + + $this->field = Enum::make('Enum')->attach(IntegerEnum::class); + } + + /** @test */ + public function it_starts_with_no_options_and_rules() + { + $field = Enum::make('Enum'); + + $this->assertArrayNotHasKey('options', $field->meta); + + $this->assertEmpty($field->rules); + } + + /** @test */ + public function it_allows_an_enum_to_be_attached() + { + $this->assertArrayHasKey('options', $this->field->meta); + } + + /** @test */ + public function it_adds_correct_rules() + { + $this->assertContains('required', $this->field->rules); + + $this->assertContainsEquals(new EnumValue(IntegerEnum::class, false), $this->field->rules); + } + + /** @test */ + public function it_displays_enum_options() + { + $this->assertCount(count(IntegerEnum::getValues()), $this->field->meta['options']); + + foreach (IntegerEnum::getValues() as $enum) { + $this->assertContains([ + 'label' => IntegerEnum::getDescription($enum), + 'value' => $enum, + ], $this->field->meta['options']); + } + } +} diff --git a/tests/Fields/FlaggedFieldTest.php b/tests/Fields/FlaggedFieldTest.php new file mode 100644 index 0000000..61d2ade --- /dev/null +++ b/tests/Fields/FlaggedFieldTest.php @@ -0,0 +1,111 @@ + true, + FlaggedEnum::WriteComments => true, + FlaggedEnum::EditComments => false, + ]; + + protected function setUp(): void + { + parent::setUp(); + + $this->setUpDatabase($this->app); + + $this->field = FlaggedEnumField::make('Enum')->attach(FlaggedEnum::class); + + $this->model = FlaggedModel::create(['enum' => FlaggedEnum::None]); + } + + /** @test */ + public function it_starts_with_no_options() + { + $field = FlaggedEnumField::make('Enum'); + + $this->assertEmpty($field->options); + } + + /** @test */ + public function it_allows_an_enum_to_be_attached() + { + $this->assertNotEmpty($this->field->options); + } + + /** @test */ + public function it_has_no_value_text() + { + $this->assertSame('None', $this->field->noValueText); + } + + /** @test */ + public function it_displays_enum_options() + { + $this->assertCount(count(FlaggedEnum::getValues()) - 1, $this->field->options); + + foreach (FlaggedEnum::getValues() as $enum) { + if ($enum === FlaggedEnum::None) { + continue; + } + + $this->assertContains([ + 'label' => FlaggedEnum::getDescription($enum), + 'name' => $enum, + ], $this->field->options); + } + } + + /** @test */ + public function it_resolves_enum_values() + { + $this->field->resolve($this->model); + + $this->assertCount(count(FlaggedEnum::getValues()) - 1, $this->field->value); + + foreach (array_keys($this->values) as $enum) { + $this->assertEquals(false, $this->field->value[$enum]); + } + + $this->model->enum = array_keys(array_filter($this->values)); + + $this->field->resolve($this->model); + + $this->assertCount(count(FlaggedEnum::getValues()) - 1, $this->field->value); + + foreach ($this->values as $enum => $value) { + $this->assertEquals($value, $this->field->value[$enum]); + } + } + + /** @test */ + public function it_fills_database_with_flagged_enum_value() + { + $request = new NovaRequest(); + $request->query->add(['enum' => json_encode($this->values)]); + + $this->field->fill($request, $this->model); + + $this->assertDatabaseHas('example_models', ['enum' => FlaggedEnum::None]); + + $this->model->save(); + + $this->assertDatabaseHas('example_models', [ + 'enum' => array_sum(array_keys(array_filter($this->values))), + ]); + + $this->assertDatabaseMissing('example_models', ['enum' => FlaggedEnum::None]); + } +} diff --git a/tests/Fields/IntegerFieldTest.php b/tests/Fields/IntegerFieldTest.php new file mode 100644 index 0000000..78010fb --- /dev/null +++ b/tests/Fields/IntegerFieldTest.php @@ -0,0 +1,60 @@ +setUpDatabase($this->app); + + $this->field = Enum::make('Enum')->attach(IntegerEnum::class); + + $this->model = IntegerModel::create(['enum' => IntegerEnum::Moderator]); + } + + /** @test */ + public function it_resolves_enum_value() + { + $this->field->resolve($this->model); + + $this->assertSame(IntegerEnum::Moderator, $this->field->value); + } + + /** @test */ + public function it_displays_enum_description() + { + $this->field->resolveForDisplay($this->model); + + $this->assertSame(IntegerEnum::Moderator()->description, $this->field->value); + } + + /** @test */ + public function it_fills_database_with_enum_value() + { + $request = new NovaRequest(); + $request->query->add(['enum' => IntegerEnum::Subscriber]); + + $this->field->fill($request, $this->model); + + $this->assertDatabaseHas('example_models', ['enum' => IntegerEnum::Moderator]); + + $this->model->save(); + + $this->assertDatabaseHas('example_models', ['enum' => IntegerEnum::Subscriber]); + + $this->assertDatabaseMissing('example_models', ['enum' => IntegerEnum::Moderator]); + } +} diff --git a/tests/Fields/NoCastsFieldTest.php b/tests/Fields/NoCastsFieldTest.php new file mode 100644 index 0000000..80c3d0b --- /dev/null +++ b/tests/Fields/NoCastsFieldTest.php @@ -0,0 +1,60 @@ +setUpDatabase($this->app, 'string'); + + $this->field = Enum::make('Enum')->attach(StringEnum::class); + + $this->model = NoCastsModel::create(['enum' => StringEnum::Moderator]); + } + + /** @test */ + public function it_resolves_enum_value() + { + $this->field->resolve($this->model); + + $this->assertSame(StringEnum::Moderator, $this->field->value); + } + + /** @test */ + public function it_displays_enum_description() + { + $this->field->resolveForDisplay($this->model); + + $this->assertSame(StringEnum::Moderator, $this->field->value); + } + + /** @test */ + public function it_fills_database_with_enum_value() + { + $request = new NovaRequest(); + $request->query->add(['enum' => StringEnum::Subscriber]); + + $this->field->fill($request, $this->model); + + $this->assertDatabaseHas('example_models', ['enum' => StringEnum::Moderator]); + + $this->model->save(); + + $this->assertDatabaseHas('example_models', ['enum' => StringEnum::Subscriber]); + + $this->assertDatabaseMissing('example_models', ['enum' => StringEnum::Moderator]); + } +} diff --git a/tests/Fields/StringFieldTest.php b/tests/Fields/StringFieldTest.php new file mode 100644 index 0000000..4ade608 --- /dev/null +++ b/tests/Fields/StringFieldTest.php @@ -0,0 +1,60 @@ +setUpDatabase($this->app, 'string'); + + $this->field = Enum::make('Enum')->attach(StringEnum::class); + + $this->model = StringModel::create(['enum' => StringEnum::Moderator]); + } + + /** @test */ + public function it_resolves_enum_value() + { + $this->field->resolve($this->model); + + $this->assertSame(StringEnum::Moderator, $this->field->value); + } + + /** @test */ + public function it_displays_enum_description() + { + $this->field->resolveForDisplay($this->model); + + $this->assertSame(StringEnum::Moderator()->description, $this->field->value); + } + + /** @test */ + public function it_fills_database_with_enum_value() + { + $request = new NovaRequest(); + $request->query->add(['enum' => StringEnum::Subscriber]); + + $this->field->fill($request, $this->model); + + $this->assertDatabaseHas('example_models', ['enum' => StringEnum::Moderator]); + + $this->model->save(); + + $this->assertDatabaseHas('example_models', ['enum' => StringEnum::Subscriber]); + + $this->assertDatabaseMissing('example_models', ['enum' => StringEnum::Moderator]); + } +} diff --git a/tests/Filters/BooleanFilterTest.php b/tests/Filters/BooleanFilterTest.php new file mode 100644 index 0000000..3bcc989 --- /dev/null +++ b/tests/Filters/BooleanFilterTest.php @@ -0,0 +1,42 @@ +filter = new EnumBooleanFilter('enum', IntegerEnum::class); + + $this->mockFilter = new MockFilter($this->filter); + } + + /** @test */ + public function it_is_a_boolean_filter() + { + $this->mockFilter->assertBooleanFilter(); + } + + /** @test */ + public function it_has_a_default_name() + { + $this->assertEquals('Enum', $this->filter->name()); + } + + /** @test */ + public function it_can_have_a_different_name() + { + $this->assertInstanceOf(EnumBooleanFilter::class, $this->filter->name('Different name')); + + $this->assertEquals('Different name', $this->filter->name()); + } +} diff --git a/tests/Filters/FilterTest.php b/tests/Filters/FilterTest.php new file mode 100644 index 0000000..1d16ad9 --- /dev/null +++ b/tests/Filters/FilterTest.php @@ -0,0 +1,42 @@ +filter = new EnumFilter('enum', IntegerEnum::class); + + $this->mockFilter = new MockFilter($this->filter); + } + + /** @test */ + public function it_is_a_select_filter() + { + $this->mockFilter->assertSelectFilter(); + } + + /** @test */ + public function it_has_a_default_name() + { + $this->assertEquals('Enum', $this->filter->name()); + } + + /** @test */ + public function it_can_have_a_different_name() + { + $this->assertInstanceOf(EnumFilter::class, $this->filter->name('Different name')); + + $this->assertEquals('Different name', $this->filter->name()); + } +} diff --git a/tests/Filters/FlaggedBooleanFilterTest.php b/tests/Filters/FlaggedBooleanFilterTest.php new file mode 100644 index 0000000..0b3ca7f --- /dev/null +++ b/tests/Filters/FlaggedBooleanFilterTest.php @@ -0,0 +1,143 @@ +setUpDatabase($this->app); + + $this->filter = new EnumBooleanFilter('enum', FlaggedEnum::class); + + $this->mockFilter = new MockFilter($this->filter); + + $this->models[0] = FlaggedModel::create(['enum' => FlaggedEnum::None]); + + $this->models[1] = FlaggedModel::create(['enum' => FlaggedEnum::ReadComments]); + + $this->models[2] = FlaggedModel::create([ + 'enum' => array_sum([ + FlaggedEnum::ReadComments, + FlaggedEnum::WriteComments, + ]), + ]); + + $this->results = [ + FlaggedEnum::None => [0], + FlaggedEnum::ReadComments => [1, 2], + FlaggedEnum::WriteComments => [2], + FlaggedEnum::EditComments => [], + ]; + } + + private function getOptions(array $keys, array $options = [[]]): array + { + if (empty($keys)) { + return $options; + } + + $current = array_shift($keys); + $newOptions = []; + + foreach ($options as $option) { + $newOptions[] = $option + [$current => true]; + $newOptions[] = $option + [$current => false]; + } + + return $this->getOptions($keys, $newOptions); + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (array_keys($this->results) as $enum) { + $this->mockFilter->assertHasOption($enum); + } + } + + /** @test */ + public function it_can_filter_by_all_selected_options() + { + $this->filter->filterAllFlags(); + + foreach (array_keys($this->results) as $enum) { + if ($enum === FlaggedEnum::None) { + $this->mockFilter->assertOptionMissing($enum); + + continue; + } + + $this->mockFilter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results_when_filtering_by_any_flag() + { + foreach ($options = $this->getOptions(FlaggedEnum::getValues()) as $option) { + $response = $this->mockFilter->apply(IntegerModel::class, $option); + + // None selected should show all models + if (count(array_filter($option)) === 0) { + $models = array_keys($this->models); + } else { + $models = array_unique(array_merge(...array_intersect_key($this->results, array_filter($option)))); + } + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } + + /** @test */ + public function it_returns_the_correct_results_when_filtering_by_all_flags() + { + $this->filter->filterAllFlags(); + + foreach ($options = $this->getOptions(array_diff(FlaggedEnum::getValues(), [FlaggedEnum::None])) as $option) { + $response = $this->mockFilter->apply(IntegerModel::class, $option); + + // None selected should show all models + if (count(array_filter($option)) === 0) { + $models = array_keys($this->models); + } else { + $models = array_intersect(array_keys($this->models), ...array_intersect_key($this->results, array_filter($option))); + } + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/FlaggedFilterTest.php b/tests/Filters/FlaggedFilterTest.php new file mode 100644 index 0000000..7d185eb --- /dev/null +++ b/tests/Filters/FlaggedFilterTest.php @@ -0,0 +1,71 @@ +setUpDatabase($this->app); + + $this->filter = new MockFilter(new EnumFilter('enum', FlaggedEnum::class)); + + $this->models[0] = FlaggedModel::create(['enum' => FlaggedEnum::None]); + + $this->models[1] = FlaggedModel::create(['enum' => FlaggedEnum::ReadComments]); + + $this->models[2] = FlaggedModel::create([ + 'enum' => array_sum([ + FlaggedEnum::ReadComments, + FlaggedEnum::WriteComments, + ]), + ]); + + $this->results = [ + FlaggedEnum::None => [0], + FlaggedEnum::ReadComments => [1, 2], + FlaggedEnum::WriteComments => [2], + FlaggedEnum::EditComments => [], + ]; + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (FlaggedEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($this->results as $enum => $models) { + $response = $this->filter->apply(FlaggedModel::class, $enum); + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/IntegerBooleanFilterTest.php b/tests/Filters/IntegerBooleanFilterTest.php new file mode 100644 index 0000000..f87fa1a --- /dev/null +++ b/tests/Filters/IntegerBooleanFilterTest.php @@ -0,0 +1,89 @@ +setUpDatabase($this->app); + + $this->filter = new MockFilter(new EnumBooleanFilter('enum', IntegerEnum::class)); + + $this->models[0] = IntegerModel::create(['enum' => IntegerEnum::Moderator]); + + $this->models[1] = IntegerModel::create(['enum' => IntegerEnum::Moderator]); + + $this->models[2] = IntegerModel::create(['enum' => IntegerEnum::Administrator]); + + $this->results = [ + IntegerEnum::Moderator => [0, 1], + IntegerEnum::Administrator => [2], + IntegerEnum::Subscriber => [], + ]; + } + + private function getOptions(array $keys, array $options = [[]]): array + { + if (empty($keys)) { + return $options; + } + + $current = array_shift($keys); + $newOptions = []; + + foreach ($options as $option) { + $newOptions[] = $option + [$current => true]; + $newOptions[] = $option + [$current => false]; + } + + return $this->getOptions($keys, $newOptions); + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (IntegerEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($options = $this->getOptions(IntegerEnum::getValues()) as $option) { + $response = $this->filter->apply(IntegerModel::class, $option); + + // None selected should show all models + if (count(array_filter($option)) === 0) { + $models = array_keys($this->models); + } else { + $models = array_unique(array_merge(...array_intersect_key($this->results, array_filter($option)))); + } + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/IntegerFilterTest.php b/tests/Filters/IntegerFilterTest.php new file mode 100644 index 0000000..510775a --- /dev/null +++ b/tests/Filters/IntegerFilterTest.php @@ -0,0 +1,65 @@ +setUpDatabase($this->app); + + $this->filter = new MockFilter(new EnumFilter('enum', IntegerEnum::class)); + + $this->models[0] = IntegerModel::create(['enum' => IntegerEnum::Moderator]); + + $this->models[1] = IntegerModel::create(['enum' => IntegerEnum::Moderator]); + + $this->models[2] = IntegerModel::create(['enum' => IntegerEnum::Administrator]); + + $this->results = [ + IntegerEnum::Moderator => [0, 1], + IntegerEnum::Administrator => [2], + IntegerEnum::Subscriber => [], + ]; + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (IntegerEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($this->results as $enum => $models) { + $response = $this->filter->apply(IntegerModel::class, $enum); + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/NoCastsBooleanFilterTest.php b/tests/Filters/NoCastsBooleanFilterTest.php new file mode 100644 index 0000000..cbde178 --- /dev/null +++ b/tests/Filters/NoCastsBooleanFilterTest.php @@ -0,0 +1,89 @@ +setUpDatabase($this->app, 'string'); + + $this->filter = new MockFilter(new EnumBooleanFilter('enum', StringEnum::class)); + + $this->models[0] = NoCastsModel::create(['enum' => StringEnum::Moderator]); + + $this->models[1] = NoCastsModel::create(['enum' => StringEnum::Moderator]); + + $this->models[2] = NoCastsModel::create(['enum' => StringEnum::Administrator]); + + $this->results = [ + StringEnum::Moderator => [0, 1], + StringEnum::Administrator => [2], + StringEnum::Subscriber => [], + ]; + } + + private function getOptions(array $keys, array $options = [[]]): array + { + if (empty($keys)) { + return $options; + } + + $current = array_shift($keys); + $newOptions = []; + + foreach ($options as $option) { + $newOptions[] = $option + [$current => true]; + $newOptions[] = $option + [$current => false]; + } + + return $this->getOptions($keys, $newOptions); + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (StringEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($options = $this->getOptions(StringEnum::getValues()) as $option) { + $response = $this->filter->apply(NoCastsModel::class, $option); + + // None selected should show all models + if (count(array_filter($option)) === 0) { + $models = array_keys($this->models); + } else { + $models = array_unique(array_merge(...array_values(array_intersect_key($this->results, array_filter($option))))); + } + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/NoCastsFilterTest.php b/tests/Filters/NoCastsFilterTest.php new file mode 100644 index 0000000..e2b7db6 --- /dev/null +++ b/tests/Filters/NoCastsFilterTest.php @@ -0,0 +1,65 @@ +setUpDatabase($this->app, 'string'); + + $this->filter = new MockFilter(new EnumFilter('enum', StringEnum::class)); + + $this->models[0] = NoCastsModel::create(['enum' => StringEnum::Moderator]); + + $this->models[1] = NoCastsModel::create(['enum' => StringEnum::Moderator]); + + $this->models[2] = NoCastsModel::create(['enum' => StringEnum::Administrator]); + + $this->results = [ + StringEnum::Moderator => [0, 1], + StringEnum::Administrator => [2], + StringEnum::Subscriber => [], + ]; + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (StringEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($this->results as $enum => $models) { + $response = $this->filter->apply(NoCastsModel::class, $enum); + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/StringBooleanFilterTest.php b/tests/Filters/StringBooleanFilterTest.php new file mode 100644 index 0000000..1a81819 --- /dev/null +++ b/tests/Filters/StringBooleanFilterTest.php @@ -0,0 +1,89 @@ +setUpDatabase($this->app, 'string'); + + $this->filter = new MockFilter(new EnumBooleanFilter('enum', StringEnum::class)); + + $this->models[0] = StringModel::create(['enum' => StringEnum::Moderator]); + + $this->models[1] = StringModel::create(['enum' => StringEnum::Moderator]); + + $this->models[2] = StringModel::create(['enum' => StringEnum::Administrator]); + + $this->results = [ + StringEnum::Moderator => [0, 1], + StringEnum::Administrator => [2], + StringEnum::Subscriber => [], + ]; + } + + private function getOptions(array $keys, array $options = [[]]): array + { + if (empty($keys)) { + return $options; + } + + $current = array_shift($keys); + $newOptions = []; + + foreach ($options as $option) { + $newOptions[] = $option + [$current => true]; + $newOptions[] = $option + [$current => false]; + } + + return $this->getOptions($keys, $newOptions); + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (StringEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($options = $this->getOptions(StringEnum::getValues()) as $option) { + $response = $this->filter->apply(StringModel::class, $option); + + // None selected should show all models + if (count(array_filter($option)) === 0) { + $models = array_keys($this->models); + } else { + $models = array_unique(array_merge(...array_values(array_intersect_key($this->results, array_filter($option))))); + } + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/Filters/StringFilterTest.php b/tests/Filters/StringFilterTest.php new file mode 100644 index 0000000..aaa07b9 --- /dev/null +++ b/tests/Filters/StringFilterTest.php @@ -0,0 +1,65 @@ +setUpDatabase($this->app, 'string'); + + $this->filter = new MockFilter(new EnumFilter('enum', StringEnum::class)); + + $this->models[0] = StringModel::create(['enum' => StringEnum::Moderator]); + + $this->models[1] = StringModel::create(['enum' => StringEnum::Moderator]); + + $this->models[2] = StringModel::create(['enum' => StringEnum::Administrator]); + + $this->results = [ + StringEnum::Moderator => [0, 1], + StringEnum::Administrator => [2], + StringEnum::Subscriber => [], + ]; + } + + /** @test */ + public function it_contains_all_the_filter_values() + { + foreach (StringEnum::getValues() as $enum) { + $this->filter->assertHasOption($enum); + } + } + + /** @test */ + public function it_returns_the_correct_results() + { + foreach ($this->results as $enum => $models) { + $response = $this->filter->apply(StringModel::class, $enum); + + $response->assertCount(count($models)); + + foreach ($models as $contain) { + $response->assertContains($this->models[$contain]); + } + + foreach (array_diff(array_keys($this->models), $models) as $missing) { + $response->assertMissing($this->models[$missing]); + } + } + } +} diff --git a/tests/IntegerEnumTest.php b/tests/IntegerEnumTest.php deleted file mode 100644 index be8366d..0000000 --- a/tests/IntegerEnumTest.php +++ /dev/null @@ -1,79 +0,0 @@ -field = Enum::make('Enum'); - - $this->field->attachEnum(ExampleIntegerEnum::class); - } - - /** @test */ - public function field_starts_with_zero_config() - { - $field = Enum::make('Enum'); - - $this->assertEmpty($field->meta); - $this->assertEmpty($field->rules); - $this->assertNull($field->resolveCallback); - $this->assertNull($field->displayCallback); - } - - /** @test */ - public function an_enum_can_be_attached_to_the_field() - { - $this->assertArrayHasKey('options', $this->field->meta); - - $this->assertEquals([ - [ - 'label' => 'Administrator', - 'value' => 0, - ], - [ - 'label' => 'Moderator', - 'value' => 1, - ], - [ - 'label' => 'Subscriber', - 'value' => 2, - ], - ], $this->field->meta['options']); - } - - /** @test */ - public function attaching_an_enum_adds_correct_rules() - { - $this->assertContains('required', $this->field->rules); - - $this->assertContainsEquals(new EnumValue(ExampleIntegerEnum::class, false), $this->field->rules); - } - - /** @test */ - public function field_resolves_correct_value() - { - $this->field->resolve(['enum' => ExampleIntegerEnum::Moderator()]); - - $this->assertSame(1, $this->field->value); - } - - /** @test */ - public function field_displays_correct_description() - { - $this->field->resolveForDisplay(['enum' => ExampleIntegerEnum::Moderator()]); - - $this->assertSame('Moderator', $this->field->value); - } -} diff --git a/tests/ModelTest.php b/tests/ModelTest.php deleted file mode 100644 index 135aa4f..0000000 --- a/tests/ModelTest.php +++ /dev/null @@ -1,57 +0,0 @@ -model = ExampleModel::create(['enum' => ExampleIntegerEnum::Moderator()]); - } - - /** @test */ - public function field_resolves_correct_value() - { - $field = Enum::make('Enum')->attachEnum(ExampleIntegerEnum::class); - - $field->resolve($this->model); - - $this->assertSame(1, $field->value); - } - - /** @test */ - public function field_displays_correct_description() - { - $field = Enum::make('Enum')->attachEnum(ExampleIntegerEnum::class); - - $field->resolveForDisplay($this->model); - - $this->assertSame('Moderator', $field->value); - } - - /** @test */ - public function field_fills_database_with_enum_value() - { - $field = Enum::make('Enum')->attachEnum(ExampleIntegerEnum::class); - - $request = new NovaRequest(); - $request->query->add(['enum' => ExampleIntegerEnum::Subscriber()]); - - $field->fill($request, $this->model); - - $this->model->save(); - - $this->assertDatabaseHas('example_models', ['enum' => 2]); - $this->assertDatabaseMissing('example_models', ['enum' => 1]); - } -} diff --git a/tests/StringEnumTest.php b/tests/StringEnumTest.php deleted file mode 100644 index 25ab8d6..0000000 --- a/tests/StringEnumTest.php +++ /dev/null @@ -1,38 +0,0 @@ -field = Enum::make('Enum'); - - $this->field->attachEnum(ExampleStringEnum::class); - } - - /** @test */ - public function field_resolves_correct_value() - { - $this->field->resolve(['enum' => ExampleStringEnum::Moderator()]); - - $this->assertSame('moderator', $this->field->value); - } - - /** @test */ - public function field_displays_correct_description() - { - $this->field->resolveForDisplay(['enum' => ExampleStringEnum::Moderator()]); - - $this->assertSame('Moderator', $this->field->value); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 4187242..407fdb3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,27 +3,25 @@ namespace SimpleSquid\Nova\Fields\Enum\Tests; use Illuminate\Database\Schema\Blueprint; +use Laravel\Nova\NovaServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; abstract class TestCase extends Orchestra { - protected function setUp(): void + protected function getPackageProviders($app) { - parent::setUp(); - - $this->setUpDatabase($this->app); + return [ + NovaServiceProvider::class, + ]; } - /** - * @param \Illuminate\Foundation\Application $app - */ - protected function setUpDatabase($app) + protected function setUpDatabase($app, $type = 'integer') { $this->artisan('migrate:fresh'); - $app['db']->connection()->getSchemaBuilder()->create('example_models', function (Blueprint $table) { + $app['db']->connection()->getSchemaBuilder()->create('example_models', function (Blueprint $table) use ($type) { $table->increments('id'); - $table->integer('enum'); + $table->$type('enum'); }); } }