diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index ff611fc..bea1f92 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} @@ -23,6 +23,6 @@ jobs: uses: aglipanci/laravel-pint-action@2.6 - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v6 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix styling diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a6a9bee --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Run Tests + +on: + push: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit + + code-style: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + tools: cs2pr + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction + + - name: Run Laravel Pint + run: | + if [ -f vendor/bin/pint ]; then + vendor/bin/pint --test + fi diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index e0a9607..c5514c8 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: main @@ -25,7 +25,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v6 + uses: stefanzweifel/git-auto-commit-action@v7 with: branch: main commit_message: Update CHANGELOG diff --git a/composer.json b/composer.json index c71e13e..9011e6f 100644 --- a/composer.json +++ b/composer.json @@ -19,17 +19,15 @@ "php": "^8.2", "spatie/laravel-package-tools": "^1.16", "illuminate/contracts": "^10.0||^11.0||^12.0", - "livewire/livewire": "^3.6", - "openspout/openspout": "^4.30" + "livewire/livewire": "^3.6||^4.0", + "openspout/openspout": "^5.2" }, "require-dev": { "laravel/pint": "^1.14", "nunomaduro/collision": "^8.1.1||^7.10.0", "larastan/larastan": "^2.9||^3.0", "orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0", - "pestphp/pest": "^3.0", - "pestphp/pest-plugin-arch": "^3.0", - "pestphp/pest-plugin-laravel": "^3.0", + "phpunit/phpunit": "^10.5.15", "spatie/laravel-ray": "^1.35" }, "autoload": { @@ -45,15 +43,12 @@ "scripts": { "post-autoload-dump": "@composer run prepare", "prepare": "@php vendor/bin/testbench package:discover --ansi", - "test": "vendor/bin/pest", - "test-coverage": "vendor/bin/pest --coverage", + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage", "format": "vendor/bin/pint" }, "config": { - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } + "sort-packages": true }, "extra": { "laravel": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5f6ae7e..8b7baa1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,31 +1,23 @@ - - - tests + + ./tests - - - - + ./src - + + + + + + + diff --git a/src/Livewire/PenguTable.php b/src/Livewire/PenguTable.php index ba90d51..2cd66e1 100644 --- a/src/Livewire/PenguTable.php +++ b/src/Livewire/PenguTable.php @@ -194,7 +194,7 @@ public function boot(): void public function mount(): void { $this->columns = collect($this->columns()) - ->filter(fn($column) => !$column->hidden) + ->filter(fn ($column) => ! $column->hidden) ->values() ->toArray(); $this->initializeFilters(); diff --git a/src/PenguTablesServiceProvider.php b/src/PenguTablesServiceProvider.php index a1348ef..dd3bb61 100644 --- a/src/PenguTablesServiceProvider.php +++ b/src/PenguTablesServiceProvider.php @@ -13,6 +13,6 @@ public function configurePackage(Package $package): void ->name('pengutables') ->hasConfigFile() ->hasTranslations() - ->hasViews(); + ->hasViews('pengutables'); } } diff --git a/src/Traits/WithExport.php b/src/Traits/WithExport.php index b82e806..d89b9be 100644 --- a/src/Traits/WithExport.php +++ b/src/Traits/WithExport.php @@ -19,6 +19,7 @@ public function exportSelected($type) public function exportAll($type) { $query = $this->applyFilters($this->applySearch($this->query())); + return $this->exportChunked($query, $type); } @@ -45,9 +46,9 @@ protected function export($data, $type) protected function exportChunked($query, $type) { - $filename = config('pengutables.export_filename', 'export_' . now()->format('Ymd_Hi')); - $exportColumns = collect($this->columns())->filter(fn($column) => $column->hideInExport)->values()->toArray(); - $headers = collect($exportColumns)->map(fn($column) => $column->label)->toArray(); + $filename = config('pengutables.export_filename', 'export_'.now()->format('Ymd_Hi')); + $exportColumns = collect($this->columns())->filter(fn ($column) => $column->hideInExport)->values()->toArray(); + $headers = collect($exportColumns)->map(fn ($column) => $column->label)->toArray(); if (str_starts_with($type, 'csv')) { return $this->streamChunkedCsv($filename, $headers, $query, $exportColumns); @@ -72,7 +73,7 @@ protected function streamCsv($filename, $headers, $rows) $writer->close(); }, 200, [ 'Content-Type' => 'text/csv', - 'Content-Disposition' => 'attachment; filename="' . $filename . '.csv"', + 'Content-Disposition' => 'attachment; filename="'.$filename.'.csv"', 'Pragma' => 'no-cache', 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 'Expires' => '0', @@ -156,7 +157,7 @@ protected function streamChunkedExcel($filename, $headers, $query, $exportColumn $writer->close(); }, 200, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'Content-Disposition' => 'attachment; filename="' . $filename . '.xlsx"', + 'Content-Disposition' => 'attachment; filename="'.$filename.'.xlsx"', 'Pragma' => 'no-cache', 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 'Expires' => '0', diff --git a/tests/ArchTest.php b/tests/ArchTest.php deleted file mode 100644 index 87fb64c..0000000 --- a/tests/ArchTest.php +++ /dev/null @@ -1,5 +0,0 @@ -expect(['dd', 'dump', 'ray']) - ->each->not->toBeUsed(); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 5d36321..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Pest.php b/tests/Pest.php deleted file mode 100644 index 3c1c957..0000000 --- a/tests/Pest.php +++ /dev/null @@ -1,5 +0,0 @@ -in(__DIR__); diff --git a/tests/Table/ActionTest.php b/tests/Table/ActionTest.php new file mode 100644 index 0000000..1d7418d --- /dev/null +++ b/tests/Table/ActionTest.php @@ -0,0 +1,38 @@ +Click Me'); + + $this->assertInstanceOf(Action::class, $action); + } + + /** @test */ + public function it_stores_the_action_content() + { + $action = Action::make(''); + + // Check that the content is stored in the toLivewire array + $livewireData = $action->toLivewire(); + $this->assertEquals('', $livewireData['action']); + } + + /** @test */ + public function it_implements_wireable_interface() + { + $action = Action::make(''); + + $serialized = $action->toLivewire(); + $deserialized = Action::fromLivewire($serialized); + + $this->assertEquals($action->toLivewire(), $deserialized->toLivewire()); + } +} diff --git a/tests/Table/ColumnTest.php b/tests/Table/ColumnTest.php new file mode 100644 index 0000000..5056e8c --- /dev/null +++ b/tests/Table/ColumnTest.php @@ -0,0 +1,143 @@ +assertEquals('Name', $column->label); + $this->assertEquals('name', $column->key); + $this->assertFalse($column->sortable); + $this->assertFalse($column->searchable); + $this->assertFalse($column->hidden); + $this->assertTrue($column->hideInExport); + $this->assertFalse($column->html); + } + + /** @test */ + public function it_can_make_a_column_sortable() + { + $column = Column::make('Name', 'name')->sortable(); + + $this->assertTrue($column->sortable); + } + + /** @test */ + public function it_can_make_a_column_searchable() + { + $column = Column::make('Name', 'name')->searchable(); + + $this->assertTrue($column->searchable); + } + + /** @test */ + public function it_can_format_a_column_value() + { + $column = Column::make('Name', 'name')->format(function ($value) { + return strtoupper($value); + }); + + // Create a simple object to test with + $model = new class + { + public $name = 'john doe'; + }; + + $this->assertEquals('JOHN DOE', $column->getValue($model)); + } + + /** @test */ + public function it_escapes_html_by_default() + { + $column = Column::make('Name', 'name'); + + // Create a simple object with HTML content + $model = new class + { + public $name = ''; + }; + + $this->assertEquals('<script>alert("xss")</script>', $column->getValue($model)); + } + + /** @test */ + public function it_does_not_escape_html_when_marked_as_html() + { + $column = Column::make('Name', 'name')->html(); + + // Create a simple object with HTML content + $model = new class + { + public $name = 'John Doe'; + }; + + $this->assertEquals('John Doe', $column->getValue($model)); + } + + /** @test */ + public function it_can_create_an_actions_column() + { + $column = Column::actions('Actions', function ($row) { + return [ + Action::make(''), + ]; + }); + + $this->assertEquals('Actions', $column->label); + $this->assertNull($column->key); + $this->assertTrue($column->html); + $this->assertFalse($column->hideInExport); // Actions should be visible in export by default + } + + /** @test */ + public function it_can_hide_a_column() + { + $column = Column::make('Name', 'name')->hidden(); + + $this->assertTrue($column->hidden); + } + + /** @test */ + public function it_can_conditionally_hide_a_column() + { + $column = Column::make('Name', 'name')->hideIf(function () { + return true; + }); + + $this->assertTrue($column->hidden); + } + + /** @test */ + public function it_can_control_visibility_in_export() + { + $column = Column::make('Name', 'name')->hideInExport(false); + + $this->assertTrue($column->hideInExport); // Note: hideInExport(false) means show in export + + $column2 = Column::make('Name', 'name')->hideInExport(true); + + $this->assertFalse($column2->hideInExport); // Note: hideInExport(true) means hide in export + } + + /** @test */ + public function it_implements_wireable_interface() + { + $column = Column::make('Name', 'name')->sortable()->searchable(); + + $serialized = $column->toLivewire(); + $deserialized = Column::fromLivewire($serialized); + + $this->assertEquals($column->label, $deserialized->label); + $this->assertEquals($column->key, $deserialized->key); + $this->assertEquals($column->sortable, $deserialized->sortable); + $this->assertEquals($column->searchable, $deserialized->searchable); + } +} diff --git a/tests/Table/Filters/BooleanFilterTest.php b/tests/Table/Filters/BooleanFilterTest.php new file mode 100644 index 0000000..b1476b8 --- /dev/null +++ b/tests/Table/Filters/BooleanFilterTest.php @@ -0,0 +1,52 @@ +assertEquals('active', $filter->key); + $this->assertEquals('active', $filter->label); + $this->assertEquals('select', $filter->type); + + // Check default options + $this->assertArrayHasKey('', $filter->options); + $this->assertArrayHasKey('true', $filter->options); + $this->assertArrayHasKey('false', $filter->options); + } + + /** @test */ + public function it_can_set_custom_labels() + { + $filter = BooleanFilter::make('active') + ->allLabel('All Items') + ->trueLabel('Active') + ->falseLabel('Inactive'); + + $this->assertEquals('All Items', $filter->options['']); + $this->assertEquals('Active', $filter->options['true']); + $this->assertEquals('Inactive', $filter->options['false']); + } + + /** @test */ + public function it_has_default_filter_callback() + { + $filter = BooleanFilter::make('verified'); + + // Verify that a callback was set + $reflection = new \ReflectionClass($filter); + $property = $reflection->getProperty('callback'); + $property->setAccessible(true); + $callback = $property->getValue($filter); + + $this->assertNotNull($callback); + $this->assertInstanceOf(\Closure::class, $callback); + } +} diff --git a/tests/Table/OptionsTest.php b/tests/Table/OptionsTest.php new file mode 100644 index 0000000..d3eb988 --- /dev/null +++ b/tests/Table/OptionsTest.php @@ -0,0 +1,110 @@ +assertEquals(10, $options->perPage); + $this->assertTrue($options->withExport); + $this->assertTrue($options->bulkActions); + $this->assertTrue($options->searchable); + $this->assertTrue($options->showItemsPerPage); + $this->assertTrue($options->loading); + $this->assertEquals('id', $options->primaryKey); + $this->assertEquals([10, 25, 50, 100], $options->perPageOptions); + $this->assertEquals(2, $options->paginationPages); + } + + /** @test */ + public function it_can_set_per_page() + { + $options = Options::make()->setPerPage(25); + + $this->assertEquals(25, $options->perPage); + } + + /** @test */ + public function it_can_set_pagination_pages() + { + $options = Options::make()->setPaginationPages(5); + + $this->assertEquals(5, $options->paginationPages); + } + + /** @test */ + public function it_can_set_per_page_options() + { + $options = Options::make()->setPerPageOptions([15, 30, 60]); + + $this->assertEquals([15, 30, 60], $options->perPageOptions); + } + + /** @test */ + public function it_can_set_primary_key() + { + $options = Options::make()->setPrimaryKey('uuid'); + + $this->assertEquals('uuid', $options->primaryKey); + } + + /** @test */ + public function it_can_disable_export() + { + $options = Options::make()->withExport(false); + + $this->assertFalse($options->withExport); + $this->assertEmpty($options->exportTypes); + } + + /** @test */ + public function it_can_set_specific_export_types() + { + $options = Options::make()->withExport(true, ExportTypes::CSV_ALL, ExportTypes::XLSX_ALL); + + $this->assertTrue($options->withExport); + $this->assertCount(2, $options->exportTypes); + $this->assertContains(ExportTypes::CSV_ALL, $options->exportTypes); + $this->assertContains(ExportTypes::XLSX_ALL, $options->exportTypes); + } + + /** @test */ + public function it_can_disable_bulk_actions() + { + $options = Options::make()->withBulkActions(false); + + $this->assertFalse($options->bulkActions); + } + + /** @test */ + public function it_can_disable_search() + { + $options = Options::make()->withSearch(false); + + $this->assertFalse($options->searchable); + } + + /** @test */ + public function it_can_disable_show_items_per_page() + { + $options = Options::make()->withShowItemsPerPage(false); + + $this->assertFalse($options->showItemsPerPage); + } + + /** @test */ + public function it_can_disable_loading_indicator() + { + $options = Options::make()->withLoading(false); + + $this->assertFalse($options->loading); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 9703c03..8fda6b4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,21 +2,11 @@ namespace RealZone22\PenguTables\Tests; -use Illuminate\Database\Eloquent\Factories\Factory; -use Orchestra\Testbench\TestCase as Orchestra; +use Orchestra\Testbench\TestCase as BaseTestCase; use RealZone22\PenguTables\PenguTablesServiceProvider; -class TestCase extends Orchestra +abstract class TestCase extends BaseTestCase { - protected function setUp(): void - { - parent::setUp(); - - Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'RealZone22\\PenguTables\\Database\\Factories\\'.class_basename($modelName).'Factory' - ); - } - protected function getPackageProviders($app) { return [ @@ -24,14 +14,14 @@ protected function getPackageProviders($app) ]; } - public function getEnvironmentSetUp($app) + protected function getEnvironmentSetUp($app) { - config()->set('database.default', 'testing'); - - /* - foreach (\Illuminate\Support\Facades\File::allFiles(__DIR__ . '/database/migrations') as $migration) { - (include $migration->getRealPath())->up(); - } - */ + // Setup default database to use sqlite :memory: + $app['config']->set('database.default', 'testbench'); + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); } } diff --git a/tests/Traits/WithExportTest.php b/tests/Traits/WithExportTest.php new file mode 100644 index 0000000..080e454 --- /dev/null +++ b/tests/Traits/WithExportTest.php @@ -0,0 +1,71 @@ +format(function ($value) { + return ''.$value.''; + })->html(), + ]; + } + }; + + // Create a mock model + $model = new class + { + public $email = 'john@example.com'; + }; + + // Get the columns + $columns = $mock->columns(); + $emailColumn = $columns[0]; + + // Test that HTML is preserved when html() is set + $formattedValue = $emailColumn->getValue($model); + $this->assertEquals('john@example.com', $formattedValue); + + // Test that strip_tags works as expected (as used in WithExport trait) + $strippedValue = strip_tags($formattedValue); + $this->assertEquals('john@example.com', $strippedValue); + } + + /** @test */ + public function it_correctly_filters_columns_for_export() + { + // Create columns with different hideInExport settings + $column1 = Column::make('ID', 'id')->hideInExport(false); // Should be INCLUDED in export (!false = true) + $column2 = Column::make('Name', 'name'); // Default hideInExport = true, so EXCLUDED + $column3 = Column::make('Email', 'email')->hideInExport(true); // Should be EXCLUDED in export (!true = false) + + $columns = [$column1, $column2, $column3]; + + // Check the actual values + $this->assertTrue($column1->hideInExport); // hideInExport(false) sets it to true + $this->assertTrue($column2->hideInExport); // Default is true + $this->assertFalse($column3->hideInExport); // hideInExport(true) sets it to false + + // Filter columns as done in WithExport trait + $exportColumns = collect($columns)->filter(fn ($column) => $column->hideInExport)->values()->toArray(); + + // Columns with hideInExport = true should be included (column1 and column2) + $this->assertCount(2, $exportColumns); + $this->assertEquals('ID', $exportColumns[0]->label); + $this->assertEquals('Name', $exportColumns[1]->label); + } +}