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);
+ }
+}