diff --git a/phpstan.neon b/phpstan.neon index a0478e61..2d639ed9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,8 +6,6 @@ parameters: ignoreErrors: - '#Cannot call method [a-zA-Z0-9\\_]#' - '#Variable \$title might not be defined.#' - - '#Variable \$thousands might not be defined.#' - - '#Variable \$decimal might not be defined.#' - '~^Parameter #1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$~' - '~^Parameter #1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$~' - '#^Method .*::fromLivewire\(\) has no return type specified\.#' diff --git a/src/Components/Filters/Builders/Number.php b/src/Components/Filters/Builders/Number.php index cd5b071d..4da53cb1 100644 --- a/src/Components/Filters/Builders/Number.php +++ b/src/Components/Filters/Builders/Number.php @@ -6,14 +6,12 @@ use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Collection; -use function Livewire\store; - class Number extends BuilderBase { public function builder(Builder|QueryBuilder $builder, string $field, int|array|string|null $values): void { - $thousands = store($this->component)->get('filters.number.' . $field . '.thousands'); - $decimal = store($this->component)->get('filters.number.' . $field . '.decimal'); + $thousands = data_get($this->filterBase, 'thousands'); + $decimal = data_get($this->filterBase, 'decimal'); if (data_get($this->filterBase, 'builder')) { /** @var \Closure $closure */ @@ -28,11 +26,11 @@ public function builder(Builder|QueryBuilder $builder, string $field, int|array| if (isset($values['start']) && !isset($values['end'])) { $start = $values['start']; - if (isset($thousands)) { + if (is_string($thousands)) { $start = str_replace($thousands, '', $start); } - if (isset($decimal)) { + if (is_string($decimal)) { $start = str_replace($decimal, '.', $start); } @@ -42,11 +40,11 @@ public function builder(Builder|QueryBuilder $builder, string $field, int|array| if (!isset($values['start']) && isset($values['end'])) { $end = $values['end']; - if (isset($decimal)) { + if (is_string($thousands)) { $end = str_replace($thousands, '', $values['end']); } - if (isset($decimal)) { + if (is_string($decimal)) { $end = (float) str_replace($decimal, '.', $end); } @@ -57,12 +55,12 @@ public function builder(Builder|QueryBuilder $builder, string $field, int|array| $start = $values['start']; $end = $values['end']; - if (isset($thousands)) { + if (is_string($thousands)) { $start = str_replace($thousands, '', $values['start']); $end = str_replace($thousands, '', $values['end']); } - if (isset($decimal)) { + if (is_string($decimal)) { $start = str_replace($decimal, '.', $start); $end = str_replace($decimal, '.', $end); } @@ -73,8 +71,8 @@ public function builder(Builder|QueryBuilder $builder, string $field, int|array| public function collection(Collection $collection, string $field, int|array|string|null $values): Collection { - $thousands = store($this->component)->get('filters.number.' . $field . '.thousands'); - $decimal = store($this->component)->get('filters.number.' . $field . '.decimal'); + $thousands = data_get($this->filterBase, 'thousands'); + $decimal = data_get($this->filterBase, 'decimal'); if (data_get($this->filterBase, 'collection')) { /** @var \Closure $closure */ @@ -87,11 +85,11 @@ public function collection(Collection $collection, string $field, int|array|stri if (isset($values['start']) && !isset($values['end'])) { $start = $values['start']; - if (isset($thousands)) { + if (is_string($thousands)) { $start = str_replace($thousands, '', $values['start']); } - if (isset($decimal)) { + if (is_string($decimal)) { $start = (float) str_replace($decimal, '.', $start); } @@ -101,11 +99,11 @@ public function collection(Collection $collection, string $field, int|array|stri if (!isset($values['start']) && isset($values['end'])) { $end = $values['end']; - if (isset($thousands)) { + if (is_string($thousands)) { $end = str_replace($thousands, '', $values['end']); } - if (isset($decimal)) { + if (is_string($decimal)) { $end = (float) str_replace($decimal, '.', $end); } @@ -116,12 +114,12 @@ public function collection(Collection $collection, string $field, int|array|stri $start = $values['start']; $end = $values['end']; - if (isset($thousands)) { + if (is_string($thousands)) { $start = str_replace($thousands, '', $values['start']); $end = str_replace($thousands, '', $values['end']); } - if (isset($decimal)) { + if (is_string($decimal)) { $start = str_replace($decimal, '.', $start); $end = str_replace($decimal, '.', $end); } diff --git a/src/Concerns/Filter.php b/src/Concerns/Filter.php index e8450f08..32bf89cf 100644 --- a/src/Concerns/Filter.php +++ b/src/Concerns/Filter.php @@ -6,8 +6,6 @@ use Illuminate\Support\{Arr, Carbon}; use Livewire\Attributes\On; -use function Livewire\store; - trait Filter { public array $filters = []; @@ -169,9 +167,6 @@ public function filterNumberStart(string $field, array $params, string $value): $this->resetPage(); - store($this)->set('filters.number.' . $field . '.thousands', $thousands); - store($this)->set('filters.number.' . $field . '.decimal', $decimal); - $this->addEnabledFilters($field, $title); if (blank($value)) { @@ -189,9 +184,6 @@ public function filterNumberEnd(string $field, array $params, string $value): vo $this->resetPage(); - store($this)->set('filters.number.' . $field . '.thousands', $thousands); - store($this)->set('filters.number.' . $field . '.decimal', $decimal); - $this->addEnabledFilters($field, $title); if (blank($value)) { diff --git a/tests/Concerns/Components/ComponentsForFilterTest.php b/tests/Concerns/Components/ComponentsForFilterTest.php new file mode 100644 index 00000000..9c2c68c7 --- /dev/null +++ b/tests/Concerns/Components/ComponentsForFilterTest.php @@ -0,0 +1,45 @@ +placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::number('price') ->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::inputText('name')->placeholder('dish_name_xyz_placeholder')->operators(), + Filter::number('price')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::boolean('in_stock'), + ]; + } +}; + +$componentQueryBuilder = new class () extends DishesQueryBuilderTable { + public function filters(): array + { + return [ + Filter::number('price_BRL')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::number('price') ->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::inputText('name')->placeholder('dish_name_xyz_placeholder')->operators(), + Filter::number('price')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::boolean('in_stock'), + ]; + } +}; + +$componentJoin = new class () extends DishesTableWithJoin { + public function filters(): array + { + return [ + Filter::number('price_BRL') ->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::inputText('dish_name')->placeholder('dish_name_xyz_placeholder')->operators(), + Filter::number('price')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands("'")->decimal(','), + Filter::boolean('in_stock'), + ]; + } +}; diff --git a/tests/Concerns/TestDatabase.php b/tests/Concerns/TestDatabase.php index da2e4879..b4f0cdc6 100644 --- a/tests/Concerns/TestDatabase.php +++ b/tests/Concerns/TestDatabase.php @@ -210,7 +210,7 @@ public static function generate(): array 'name' => 'Barco-Sushi da Sueli', 'category_id' => 1, 'chef_id' => 1, - 'price' => 70.00, + 'price' => 5000.00, 'in_stock' => false, 'produced_at' => '2021-07-07 19:59:59', 'additional' => json_encode([ @@ -224,7 +224,7 @@ public static function generate(): array 'name' => 'Barco-Sushi Simples', 'category_id' => 1, 'chef_id' => 1, - 'price' => 80.40, + 'price' => 1500.40, 'in_stock' => false, 'produced_at' => '2021-08-08 00:00:00', 'additional' => json_encode([ @@ -239,7 +239,7 @@ public static function generate(): array 'name' => 'Polpetone Filé Mignon', 'category_id' => 1, 'chef_id' => 1, - 'price' => 90.10, + 'price' => 5000.00, 'in_stock' => false, 'produced_at' => '2021-09-09 00:00:00', ], @@ -247,7 +247,7 @@ public static function generate(): array 'name' => 'борщ', 'category_id' => 7, 'chef_id' => 1, - 'price' => 100.90, + 'price' => 5000.00, 'in_stock' => false, 'produced_at' => '2021-10-10 00:00:00', ], diff --git a/tests/Datasets/FilterComponent.php b/tests/Datasets/FilterComponent.php new file mode 100644 index 00000000..c9c26c79 --- /dev/null +++ b/tests/Datasets/FilterComponent.php @@ -0,0 +1,10 @@ + id' => [$component::class, (object) ['theme' => 'tailwind', 'field' => 'name']], + 'bootstrap -> id' => [$component::class, (object) ['theme' => 'bootstrap', 'field' => 'name']], + 'tailwind -> dishes.id' => [$componentJoin::class, (object) ['theme' => 'tailwind', 'field' => 'dishes.name']], + 'bootstrap -> dishes.id' => [$componentJoin::class, (object) ['theme' => 'bootstrap', 'field' => 'dishes.name']], +]); diff --git a/tests/Feature/Filters/FilterInputTextTest.php b/tests/Feature/Filters/FilterInputTextTest.php new file mode 100644 index 00000000..4ae71bf1 --- /dev/null +++ b/tests/Feature/Filters/FilterInputTextTest.php @@ -0,0 +1,65 @@ +call($params->theme); + + /** @var PowerGridComponent $component */ + expect($component->filters) + ->toMatchArray([]); + + $component->set('filters', filterInputText('ba', 'contains', $params->field)); + + if (str_contains($params->field, '.')) { + $data = Str::of($params->field)->explode('.'); + $table = $data->get(0); + $field = $data->get(1); + + expect($component->filters) + ->toMatchArray([ + 'input_text' => [ + $table => [ + $field => 'ba', + ], + ], + 'input_text_options' => [ + $table => [ + $field => 'contains', + ], + ], + ]); + } else { + expect($component->filters) + ->toMatchArray([ + 'input_text' => [ + $params->field => 'ba', + ], + 'input_text_options' => [ + $params->field => 'contains', + ], + ]); + } + + $component->assertSee('Barco-Sushi da Sueli') + ->assertSeeHtml('dish_name_xyz_placeholder'); + + $filters = array_merge($component->filters, filterNumber('price', min: '1\'500.20', max: '3\'000.00')); + + $component->set('filters', $filters) + ->assertSeeHtml('placeholder="min_xyz_placeholder"') + ->assertSeeHtml('placeholder="max_xyz_placeholder"') + ->assertSee('Barco-Sushi Simples') + ->assertDontSee('Barco-Sushi da Sueli') + ->assertDontSee('Polpetone Filé Mignon') + ->assertDontSee('борщ'); + + expect($component->filters)->toBe($filters); +})->group('filters') +->with('filterComponent'); diff --git a/tests/Feature/Filters/FilterMultipleTest.php b/tests/Feature/Filters/FilterMultipleTest.php deleted file mode 100644 index cdcf58c8..00000000 --- a/tests/Feature/Filters/FilterMultipleTest.php +++ /dev/null @@ -1,133 +0,0 @@ -placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.') ->decimal(','), - Filter::number('price') ->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.') ->decimal(','), - Filter::inputText('name')->placeholder('dish_name_xyz_placeholder')->operators(), - Filter::number('price')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.')->decimal(','), - Filter::boolean('in_stock'), - ]; - } -}; - -$componentQueryBuilder = new class () extends DishesQueryBuilderTable { - public function filters(): array - { - return [ - Filter::number('price_BRL')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.') ->decimal(','), - Filter::number('price') ->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.') ->decimal(','), - Filter::inputText('name')->placeholder('dish_name_xyz_placeholder')->operators(), - Filter::number('price')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.')->decimal(','), - Filter::boolean('in_stock'), - ]; - } -}; - -$componentJoin = new class () extends DishesTableWithJoin { - public function filters(): array - { - return [ - Filter::number('price_BRL') ->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.') ->decimal(','), - Filter::inputText('dish_name')->placeholder('dish_name_xyz_placeholder')->operators(), - Filter::number('price')->placeholder('min_xyz_placeholder', 'max_xyz_placeholder')->thousands('.')->decimal(','), - Filter::boolean('in_stock'), - ]; - } -}; - -it('properly filters by inputText, number, boolean filter and clearAll', function (string $component, object $params) { - $component = livewire($component) - ->call($params->theme); - - /** @var PowerGridComponent $component */ - expect($component->filters) - ->toMatchArray([]); - - $component->set('filters', filterInputText('ba', 'contains', $params->field)); - - if (str_contains($params->field, '.')) { - $data = Str::of($params->field)->explode('.'); - $table = $data->get(0); - $field = $data->get(1); - - expect($component->filters) - ->toMatchArray([ - 'input_text' => [ - $table => [ - $field => 'ba', - ], - ], - 'input_text_options' => [ - $table => [ - $field => 'contains', - ], - ], - ]); - } else { - expect($component->filters) - ->toMatchArray([ - 'input_text' => [ - $params->field => 'ba', - ], - 'input_text_options' => [ - $params->field => 'contains', - ], - ]); - } - - $component->assertSee('Barco-Sushi da Sueli') - ->assertSeeHtml('dish_name_xyz_placeholder'); - - $filters = array_merge($component->filters, filterNumber('price', '80.00', '100')); - - $component->set('filters', $filters) - ->assertSeeHtml('placeholder="min_xyz_placeholder"') - ->assertSeeHtml('placeholder="max_xyz_placeholder"') - ->assertDontSee('Barco-Sushi da Sueli') - ->assertSee('Barco-Sushi Simples') - ->assertDontSee('Polpetone Filé Mignon') - ->assertDontSee('борщ'); - - expect($component->filters) - ->toMatchArray($filters); - - $filters = array_merge($component->filters, filterBoolean('in_stock', 'true')); - - $component->set('filters', $filters) - ->assertDontSee('Barco-Sushi Simples'); - - expect($component->filters) - ->toMatchArray($filters); - - $component->call('clearFilter', $params->field); - - $component->assertDontSee('Polpetone Filé Mignon'); - - $component->call('clearAllFilters'); - - $component->assertSee('Barco-Sushi da Sueli') - ->assertSee('Barco-Sushi Simples') - ->assertSee('Polpetone Filé Mignon') - ->assertSee('борщ'); - expect($component->filters) - ->toMatchArray([]); -})->group('filters') - ->with([ - 'tailwind -> id' => [$component::class, (object) ['theme' => 'tailwind', 'field' => 'name']], - 'bootstrap -> id' => [$component::class, (object) ['theme' => 'bootstrap', 'field' => 'name']], - 'tailwind -> dishes.id' => [$componentJoin::class, (object) ['theme' => 'tailwind', 'field' => 'dishes.name']], - 'bootstrap -> dishes.id' => [$componentJoin::class, (object) ['theme' => 'bootstrap', 'field' => 'dishes.name']], - ]); diff --git a/tests/Feature/Filters/FilterNumberTest.php b/tests/Feature/Filters/FilterNumberTest.php new file mode 100644 index 00000000..74aad495 --- /dev/null +++ b/tests/Feature/Filters/FilterNumberTest.php @@ -0,0 +1,37 @@ +call($params->theme); + + $filters = array_merge($component->filters, filterNumber('price', min: '1\'500.20', max: '3\'000.00')); + + $component->set('filters', $filters) + ->assertSeeHtml('placeholder="min_xyz_placeholder"') + ->assertSeeHtml('placeholder="max_xyz_placeholder"') + ->assertSee('Barco-Sushi Simples') + ->assertDontSee('Barco-Sushi da Sueli') + ->assertDontSee('Polpetone Filé Mignon') + ->assertDontSee('борщ'); + + expect($component->filters)->toBe($filters); +})->group('filters') +->with('filterComponent'); + +it('properly filters by filter Number with wrong separators', function (string $component, object $params) { + $component = livewire($component) + ->call($params->theme); + + // Use wrong separators + $filters = array_merge($component->filters, filterNumber('price', min: '1@500#20', max: '3@000#00')); + + $component->set('filters', $filters) + ->assertSee('No records found'); +}) +->skipOnPostgreSQL('PG will throw "invalid input syntax for type double precision"') +->group('filters') +->with('filterComponent'); diff --git a/tests/Feature/Filters/MultipleFiltersTest.php b/tests/Feature/Filters/MultipleFiltersTest.php new file mode 100644 index 00000000..b58d9315 --- /dev/null +++ b/tests/Feature/Filters/MultipleFiltersTest.php @@ -0,0 +1,86 @@ +call($params->theme); + + /** @var PowerGridComponent $component */ + expect($component->filters) + ->toMatchArray([]); + + $component->set('filters', filterInputText('ba', 'contains', $params->field)); + + if (str_contains($params->field, '.')) { + $data = Str::of($params->field)->explode('.'); + $table = $data->get(0); + $field = $data->get(1); + + expect($component->filters) + ->toMatchArray([ + 'input_text' => [ + $table => [ + $field => 'ba', + ], + ], + 'input_text_options' => [ + $table => [ + $field => 'contains', + ], + ], + ]); + } else { + expect($component->filters) + ->toMatchArray([ + 'input_text' => [ + $params->field => 'ba', + ], + 'input_text_options' => [ + $params->field => 'contains', + ], + ]); + } + + $component->assertSee('Barco-Sushi da Sueli') + ->assertSeeHtml('dish_name_xyz_placeholder'); + + $filters = array_merge($component->filters, filterNumber('price', min: '1\'500.20', max: '3\'000.00')); + + $component->set('filters', $filters) + ->assertSeeHtml('placeholder="min_xyz_placeholder"') + ->assertSeeHtml('placeholder="max_xyz_placeholder"') + ->assertSee('Barco-Sushi Simples') + ->assertDontSee('Barco-Sushi da Sueli') + ->assertDontSee('Polpetone Filé Mignon') + ->assertDontSee('борщ'); + + expect($component->filters)->toBe($filters); + + $filters = array_merge($component->filters, filterBoolean('in_stock', 'true')); + + $component->set('filters', $filters) + ->assertDontSee('Barco-Sushi Simples'); + + expect($component->filters) + ->toMatchArray($filters); + + $component->call('clearFilter', $params->field); + + $component->assertDontSee('Polpetone Filé Mignon'); + + $component->call('clearAllFilters'); + + $component->assertSee('Barco-Sushi da Sueli') + ->assertSee('Barco-Sushi Simples') + ->assertSee('Polpetone Filé Mignon') + ->assertSee('борщ'); + expect($component->filters) + ->toMatchArray([]); +})->group('filters') +->with('filterComponent'); diff --git a/tests/Pest.php b/tests/Pest.php index e5fc043c..3703fb2b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -33,7 +33,15 @@ function powergrid(): PowerGridComponent function requiresMySQL() { if (DB::getDriverName() !== 'mysql') { - test()->markTestSkipped('This test requires MySQL database'); + test()->skipWithReason('This test requires MySQL database'); + } + + return test(); +} +function skipOnMySQL(string $reason = '') +{ + if (DB::getDriverName() === 'mysql') { + test()->skipWithReason('Skipping on MySQL', $reason); } return test(); @@ -42,16 +50,16 @@ function requiresMySQL() function requiresSQLite() { if (DB::getDriverName() !== 'sqlite') { - test()->markTestSkipped('This test requires SQLite database'); + test()->skipWithReason('This test requires SQLite database'); } return test(); } -function skipOnSQLite() +function skipOnSQLite(string $reason = '') { if (DB::getDriverName() === 'sqlite') { - test()->markTestSkipped('This test requires MYSQL/PGSQL database'); + test()->skipWithReason('Skipping on SQLite', $reason); } return test(); @@ -60,7 +68,16 @@ function skipOnSQLite() function requiresPostgreSQL() { if (DB::getDriverName() !== 'pgsql') { - test()->markTestSkipped('This test requires PostgreSQL database'); + test()->skipWithReason('This test requires PostgreSQL database'); + } + + return test(); +} + +function skipOnPostgreSQL(string $reason = '') +{ + if (DB::getDriverName() === 'pgsql') { + test()->skipWithReason('Skipping on PostgreSQL', $reason); } return test(); @@ -71,7 +88,7 @@ function requiresOpenSpout() $isInstalled = \Composer\InstalledVersions::isInstalled('openspout/openspout'); if (!$isInstalled) { - test()->markTestSkipped('This test requires openspout/openspout'); + test()->skipWithReason('test requires openspout/openspout'); } return test(); @@ -81,3 +98,12 @@ function fixturePath(string $filepath): string { return str_replace('/', DIRECTORY_SEPARATOR, __DIR__ . '/Concerns/Fixtures/' . ltrim($filepath, '/')); } + +function skipWithReason(string $default, string $reason = ''): void +{ + $reason = str($reason)->whenNotEmpty(fn ($r) => $r->prepend(': ')) + ->prepend($default) + ->toString(); + + test()->markTestSkipped($reason); +}