From c772c5819d057495cf675c80a394d3f62df59238 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Thu, 13 Nov 2025 22:09:14 +0000 Subject: [PATCH] fix: Search and sort same relationship table (#18499) * fix: Search and sort on same relationship table * Update InteractsWithTableQuery.php * add test * Update ColumnTest.php * Update Group.php * grouping tests * clean up * fix column qualification * dont use joins for ordering anything * test * fix migrations order * phpstan and summary issues * update tests * fix * fix tests * Update GroupingTest.php * Revert "fix" This reverts commit 160c3e0d18fd1f3799b7caa3582ed8bf2bec566b. * Update TestsSummaries.php * fix concurrency * Update TestsSummaries.php * Update 0009_modify_users_table.php * BelongsToThrough support * nested belongstothrough * Update ColumnTest.php * fix snapshots * fix sorting based on sql --- .env.testing.example | 21 + .github/workflows/phpstan.yml | 4 + .github/workflows/tests.yml | 47 +- .gitignore | 2 + composer.json | 10 +- .../src/Services/RelationshipOrderer.php | 284 +++++ .../Concerns/InteractsWithTableQuery.php | 48 +- .../src/Columns/Summarizers/Summarizer.php | 14 +- packages/tables/src/Grouping/Group.php | 8 +- .../tables/src/Testing/TestsSummaries.php | 4 +- phpunit.mysql.xml | 16 + phpunit.pgsql.xml | 16 + phpunit.sqlite.xml | 16 + ...erate_a_form_schema_class_for_a_model.snap | 4 +- ..._form_component_with_generated_fields.snap | 4 +- ...nerated_form_schema_and_table_columns.snap | 4 + ...nerated_form_schema_and_table_columns.snap | 4 + ...he_form_and_table_of_a_resource_class.snap | 6 +- ...nerated_form_schema_and_table_columns.snap | 5 + ...nerated_form_schema_and_table_columns.snap | 5 + ..._content_embedded_in_a_resource_class.snap | 11 +- ...an_generate_the_resource_form_content.snap | 4 +- ...enerate_the_resource_infolist_content.snap | 5 +- ...n_generate_the_resource_table_content.snap | 2 + ...enerate_a_Livewire_table_component__2.snap | 54 +- ..._for_a_model_in_a_nested_directory__2.snap | 54 +- ...le_component_in_a_nested_directory__2.snap | 54 +- ...ewire_table_component_with_a_model__2.snap | 54 +- ...able_component_with_generated_columns.snap | 2 + ...e_component_with_generated_columns__2.snap | 81 +- tests/bootstrap.php | 12 + tests/database/factories/CompanyFactory.php | 18 + tests/database/factories/ImageFactory.php | 19 + tests/database/factories/ProfileFactory.php | 22 + tests/database/factories/SettingFactory.php | 21 + ..._table.php => 0001_create_teams_table.php} | 1 + .../0002_create_companies_table.php | 22 + ....php => 0003_create_departments_table.php} | 0 ..._table.php => 0004_create_posts_table.php} | 3 +- .../migrations/0005_create_profiles_table.php | 24 + .../migrations/0006_create_settings_table.php | 24 + .../migrations/0007_create_images_table.php | 24 + ...able.php => 0008_create_tickets_table.php} | 0 ..._table.php => 0009_modify_users_table.php} | 2 +- .../0010_add_team_id_to_users_table.php | 33 + ...le.php => 0011_create_team_user_table.php} | 0 ...php => 0012_create_domain_teams_table.php} | 0 ...> 0013_create_department_ticket_table.php} | 0 ... => 0014_create_ticket_messages_table.php} | 0 tests/src/Fixtures/Livewire/PostsTable.php | 37 + .../PostsTableWithQualifiedColumns.php | 44 + tests/src/Fixtures/Livewire/UsersTable.php | 65 + .../Fixtures/Livewire/UsersWithTeamTable.php | 45 + tests/src/Fixtures/Models/Company.php | 19 + tests/src/Fixtures/Models/Image.php | 25 + tests/src/Fixtures/Models/Post.php | 12 + tests/src/Fixtures/Models/Profile.php | 42 + tests/src/Fixtures/Models/Setting.php | 25 + tests/src/Fixtures/Models/Team.php | 6 + tests/src/Fixtures/Models/User.php | 25 + .../Forms/Commands/MakeFormCommandTest.php | 14 +- .../Commands/MakeLivewireFormCommandTest.php | 12 +- .../Legacy/LegacyMakePageCommandTest.php | 6 +- .../LegacyMakeRelationManagerCommandTest.php | 6 +- .../Legacy/LegacyMakeResourceCommandTest.php | 112 +- .../Panels/Commands/MakePageCommandTest.php | 6 +- .../MakeRelationManagerCommandTest.php | 6 +- .../Commands/MakeResourceCommandTest.php | 280 +++-- .../Commands/MakeSettingsPageCommandTest.php | 6 +- .../Resources/Pages/CreateRecordTest.php | 98 +- .../Resources/Pages/ListRecordsTest.php | 43 +- .../src/Panels/Tenancy/ResolveTenantTest.php | 2 +- tests/src/Tables/ColumnTest.php | 1066 ++++++++++++++++- .../Commands/MakeLivewireTableCommandTest.php | 70 +- tests/src/Tables/GroupingTest.php | 333 +++++ tests/src/TestCase.php | 51 +- 76 files changed, 3264 insertions(+), 260 deletions(-) create mode 100644 .env.testing.example create mode 100644 packages/support/src/Services/RelationshipOrderer.php create mode 100644 phpunit.mysql.xml create mode 100644 phpunit.pgsql.xml create mode 100644 phpunit.sqlite.xml create mode 100644 tests/bootstrap.php create mode 100644 tests/database/factories/CompanyFactory.php create mode 100644 tests/database/factories/ImageFactory.php create mode 100644 tests/database/factories/ProfileFactory.php create mode 100644 tests/database/factories/SettingFactory.php rename tests/database/migrations/{create_teams_table.php => 0001_create_teams_table.php} (91%) create mode 100644 tests/database/migrations/0002_create_companies_table.php rename tests/database/migrations/{create_departments_table.php => 0003_create_departments_table.php} (100%) rename tests/database/migrations/{create_posts_table.php => 0004_create_posts_table.php} (89%) create mode 100644 tests/database/migrations/0005_create_profiles_table.php create mode 100644 tests/database/migrations/0006_create_settings_table.php create mode 100644 tests/database/migrations/0007_create_images_table.php rename tests/database/migrations/{create_tickets_table.php => 0008_create_tickets_table.php} (100%) rename tests/database/migrations/{modify_users_table.php => 0009_modify_users_table.php} (93%) create mode 100644 tests/database/migrations/0010_add_team_id_to_users_table.php rename tests/database/migrations/{create_team_user_table.php => 0011_create_team_user_table.php} (100%) rename tests/database/migrations/{create_domain_teams_table.php => 0012_create_domain_teams_table.php} (100%) rename tests/database/migrations/{create_department_ticket_table.php => 0013_create_department_ticket_table.php} (100%) rename tests/database/migrations/{create_ticket_messages_table.php => 0014_create_ticket_messages_table.php} (100%) create mode 100644 tests/src/Fixtures/Livewire/PostsTableWithQualifiedColumns.php create mode 100644 tests/src/Fixtures/Livewire/UsersTable.php create mode 100644 tests/src/Fixtures/Livewire/UsersWithTeamTable.php create mode 100644 tests/src/Fixtures/Models/Company.php create mode 100644 tests/src/Fixtures/Models/Image.php create mode 100644 tests/src/Fixtures/Models/Profile.php create mode 100644 tests/src/Fixtures/Models/Setting.php diff --git a/.env.testing.example b/.env.testing.example new file mode 100644 index 0000000000..8ccae4caa7 --- /dev/null +++ b/.env.testing.example @@ -0,0 +1,21 @@ +# Testing Environment Configuration +# Copy this file to .env.testing and adjust values for your local setup +# +# USAGE: +# These values are automatically loaded by PHPUnit's bootstrap file. +# Use database-specific variables to configure ports and credentials for each database. + +# Default Database Configuration +DB_HOST=127.0.0.1 +# DB_DATABASE is intentionally not set here - let TestCase.php defaults handle it +# (SQLite needs ":memory:", MySQL/Postgres need "testing") + +# PostgreSQL-specific configuration (for non-standard ports) +# PGSQL_PORT=5432 +# PGSQL_USERNAME=postgres +# PGSQL_PASSWORD= + +# MySQL-specific configuration (uncomment if needed) +# MYSQL_PORT=3306 +# MYSQL_USERNAME=root +# MYSQL_PASSWORD= diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 69ca927133..633dd3ad11 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -4,6 +4,10 @@ on: push: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: phpstan: runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb288eb3bd..3e8541af7a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,10 @@ on: push: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: run-tests: runs-on: ubuntu-latest @@ -12,13 +16,32 @@ jobs: matrix: php: [8.4, 8.3, 8.2] laravel: [12.*, 11.*] + db: [sqlite, mysql, pgsql] dependency-version: [prefer-stable] include: - laravel: 12.* testbench: 10.* - laravel: 11.* testbench: 9.* - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + name: P${{ matrix.php }} - L${{ matrix.laravel }} - DB:${{ matrix.db }} + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: testing + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: testing + ports: + - 5432:5432 + options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v3 - name: Cache dependencies @@ -30,11 +53,29 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mbstring, pdo, pdo_sqlite + extensions: mbstring, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql coverage: none - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + - name: Wait for MySQL + if: matrix.db == 'mysql' + run: | + for i in {1..60}; do + nc -z 127.0.0.1 3306 && echo "MySQL is up" && exit 0 + echo "Waiting for MySQL ($i) ..." && sleep 2 + done + echo "MySQL did not become ready in time" && exit 1 + - name: Wait for Postgres + if: matrix.db == 'pgsql' + run: | + for i in {1..60}; do + nc -z 127.0.0.1 5432 && echo "Postgres is up" && exit 0 + echo "Waiting for Postgres ($i) ..." && sleep 2 + done + echo "Postgres did not become ready in time" && exit 1 - name: Execute tests - run: ./vendor/bin/pest + run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml + env: + DB_PASSWORD: password diff --git a/.gitignore b/.gitignore index 47c0434da5..99ff8aa08b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ composer.phar Thumbs.db phpunit.xml +.env.testing +!phpunit.*.xml diff --git a/composer.json b/composer.json index 6266594bb6..c712356956 100644 --- a/composer.json +++ b/composer.json @@ -84,9 +84,17 @@ "pint": "pint --config pint-strict-imports.json", "rector": "rector", "test:pest": "pest --parallel", + "test:sqlite": "pest --configuration=phpunit.sqlite.xml --parallel", + "test:mysql": "pest --configuration=phpunit.mysql.xml", + "test:pgsql": "pest --configuration=phpunit.pgsql.xml", + "test:all-databases": [ + "@test:sqlite", + "@test:mysql", + "@test:pgsql" + ], "test:phpstan": "phpstan analyse", "test": [ - "@test:pest", + "@test:sqlite", "@test:phpstan" ] }, diff --git a/packages/support/src/Services/RelationshipOrderer.php b/packages/support/src/Services/RelationshipOrderer.php new file mode 100644 index 0000000000..de2a7a9277 --- /dev/null +++ b/packages/support/src/Services/RelationshipOrderer.php @@ -0,0 +1,284 @@ +buildRelationshipChain($query->getModel(), $relationshipName); + $targetModel = $this->getTargetModel($relationshipChain); + + $subquery = $this->initializeSubquery($targetModel, $column); + $this->applyRelationshipConstraints($subquery, $relationshipChain, $query->getModel()); + + return $subquery->limit(1)->toBase(); + } + + /** + * @return array + */ + protected function buildRelationshipChain(Model $baseModel, string $relationshipPath): array + { + $relationshipSegments = explode('.', $relationshipPath); + $currentModel = $baseModel; + $chain = []; + + foreach ($relationshipSegments as $relationshipSegment) { + $relationship = $currentModel->{$relationshipSegment}(); + + $this->validateRelationshipType($relationship); + + $chain[] = $relationship; + + $currentModel = $relationship->getRelated(); + } + + return $chain; + } + + protected function validateRelationshipType(Relation $relationship): void + { + if ($relationship instanceof BelongsTo || $relationship instanceof HasOne || $relationship instanceof MorphOne || $relationship instanceof BelongsToThrough) { + return; + } + + throw new InvalidArgumentException( + 'Nested sorting only supports [BelongsTo], [HasOne], [MorphOne], and [BelongsToThrough] relationships, [' . $relationship::class . '] found.' + ); + } + + /** + * @param array $relationshipChain + */ + protected function getTargetModel(array $relationshipChain): Model + { + $lastRelationship = end($relationshipChain); + + return $lastRelationship->getRelated(); + } + + protected function initializeSubquery(Model $targetModel, string $column): EloquentBuilder + { + return $targetModel::query()->select($targetModel->qualifyColumn($column)); + } + + /** + * @param array $relationshipChain + */ + protected function applyRelationshipConstraints( + EloquentBuilder $subquery, + array $relationshipChain, + Model $baseModel + ): void { + $chainLength = count($relationshipChain); + + for ($i = $chainLength - 1; $i >= 0; $i--) { + $isFirstRelationship = $i === 0; + + if ($isFirstRelationship) { + $this->applyFirstRelationshipConstraint($subquery, $relationshipChain[$i], $baseModel); /** @phpstan-ignore argument.type */ + } else { + $this->applyIntermediateRelationshipJoin($subquery, $relationshipChain[$i], $relationshipChain[$i - 1]); /** @phpstan-ignore argument.type, argument.type */ + } + } + } + + protected function applyFirstRelationshipConstraint( + EloquentBuilder $subquery, + BelongsTo | HasOne | MorphOne | BelongsToThrough $relationship, + Model $baseModel + ): void { + $baseTable = $baseModel->getTable(); + + if ($relationship instanceof BelongsTo) { + $this->applyBelongsToConstraint($subquery, $relationship, $baseTable); + } elseif ($relationship instanceof MorphOne) { + $this->applyMorphOneConstraint($subquery, $relationship, $baseModel); + } elseif ($relationship instanceof HasOne) { + $this->applyHasOneConstraint($subquery, $relationship, $baseModel); + } elseif ($relationship instanceof BelongsToThrough) { + $this->applyBelongsToThroughConstraint($subquery, $relationship, $baseModel); + } + } + + protected function applyBelongsToConstraint( + EloquentBuilder $subquery, + BelongsTo $relationship, + string $baseTable + ): void { + $subquery->whereColumn( + $relationship->getQualifiedOwnerKeyName(), + $relationship->getQualifiedForeignKeyName(), + ); + } + + protected function applyHasOneConstraint( + EloquentBuilder $subquery, + HasOne $relationship, + Model $baseModel + ): void { + $subquery->whereColumn( + $relationship->getQualifiedForeignKeyName(), + $baseModel->qualifyColumn($relationship->getLocalKeyName()), + ); + } + + protected function applyMorphOneConstraint( + EloquentBuilder $subquery, + MorphOne $relationship, + Model $baseModel + ): void { + $subquery->whereColumn( + $relationship->getQualifiedForeignKeyName(), + $baseModel->qualifyColumn($relationship->getLocalKeyName()), + )->where( + $relationship->getQualifiedMorphType(), + $relationship->getMorphClass() + ); + } + + protected function applyBelongsToThroughConstraint( + EloquentBuilder $subquery, + BelongsToThrough $relationship, + Model $baseModel, + ): void { + $throughParents = $relationship->getThroughParents(); + + foreach ($throughParents as $i => $throughParent) { + $isFirstThroughParent = $i === 0; + + if ($isFirstThroughParent) { + $predecessor = $relationship->getRelated(); + $first = $throughParent->qualifyColumn($relationship->getForeignKeyName($predecessor)); + $second = $predecessor->qualifyColumn($relationship->getLocalKeyName($predecessor)); + + $subquery->join($throughParent->getTable(), $first, '=', $second); + } else { + $predecessor = $throughParents[$i - 1]; + $first = $throughParent->qualifyColumn($relationship->getForeignKeyName($predecessor)); + $second = $predecessor->qualifyColumn($relationship->getLocalKeyName($predecessor)); + + $subquery->join($throughParent->getTable(), $first, '=', $second); + } + } + + $subquery->whereColumn( + $relationship->getQualifiedFirstLocalKeyName(), + $baseModel->qualifyColumn($relationship->getFirstForeignKeyName()), + ); + } + + protected function applyIntermediateRelationshipJoin( + EloquentBuilder $subquery, + BelongsTo | HasOne | MorphOne | BelongsToThrough $currentRelationship, + BelongsTo | HasOne | MorphOne | BelongsToThrough $previousRelationship + ): void { + $previousTable = $previousRelationship->getRelated()->getTable(); + + if ($currentRelationship instanceof BelongsTo) { + $this->joinBelongsTo($subquery, $currentRelationship, $previousTable); + } elseif ($currentRelationship instanceof MorphOne) { + $this->joinMorphOne($subquery, $currentRelationship, $previousTable); + } elseif ($currentRelationship instanceof HasOne) { + $this->joinHasOne($subquery, $currentRelationship, $previousTable); + } elseif ($currentRelationship instanceof BelongsToThrough) { + $this->joinBelongsToThrough($subquery, $currentRelationship, $previousTable); + } + } + + protected function joinBelongsTo( + EloquentBuilder $subquery, + BelongsTo $relationship, + string $previousTable + ): void { + $subquery->join( + $previousTable, + $relationship->getQualifiedOwnerKeyName(), + '=', + $relationship->getQualifiedForeignKeyName(), + ); + } + + protected function joinHasOne( + EloquentBuilder $subquery, + HasOne $relationship, + string $previousTable + ): void { + $subquery->join( + $previousTable, + $relationship->getQualifiedForeignKeyName(), + '=', + $relationship->getQualifiedParentKeyName(), + ); + } + + protected function joinMorphOne( + EloquentBuilder $subquery, + MorphOne $relationship, + string $previousTable + ): void { + $subquery->join( + $previousTable, + $relationship->getQualifiedForeignKeyName(), + '=', + $relationship->getQualifiedParentKeyName(), + )->where( + $relationship->getQualifiedMorphType(), + $relationship->getMorphClass(), + ); + } + + protected function joinBelongsToThrough( + EloquentBuilder $subquery, + BelongsToThrough $relationship, + string $previousTable + ): void { + $throughParents = $relationship->getThroughParents(); + $targetModel = $relationship->getRelated(); + + // Join through parents from target to previousTable + // For User->Company via Team: join Team to Company, then User to Team + foreach ($throughParents as $i => $throughParent) { + $isFirstThroughParent = $i === 0; + + if ($isFirstThroughParent) { + // Join first through parent to the target model + $subquery->join( + $throughParent->getTable(), + $targetModel->qualifyColumn($relationship->getLocalKeyName($targetModel)), + '=', + $throughParent->qualifyColumn($relationship->getForeignKeyName($targetModel)), + ); + } else { + // Join subsequent through parents + $predecessor = $throughParents[$i - 1]; + $subquery->join( + $throughParent->getTable(), + $predecessor->qualifyColumn($relationship->getLocalKeyName($predecessor)), + '=', + $throughParent->qualifyColumn($relationship->getForeignKeyName($predecessor)), + ); + } + } + + // Finally, join the previous table to the last through parent + $lastThroughParent = end($throughParents); + $subquery->join( + $previousTable, + $lastThroughParent->qualifyColumn($relationship->getLocalKeyName($lastThroughParent)), + '=', + "{$previousTable}.{$relationship->getForeignKeyName($lastThroughParent)}", + ); + } +} diff --git a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php index b3f52ae6ed..157d187a10 100644 --- a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php +++ b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php @@ -2,10 +2,12 @@ namespace Filament\Tables\Columns\Concerns; +use Filament\Support\Services\RelationshipOrderer; use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Arr; +use Znck\Eloquent\Relations\BelongsToThrough; use function Filament\Support\generate_search_column_expression; use function Filament\Support\generate_search_term_expression; @@ -87,14 +89,28 @@ trait InteractsWithTableQuery fn (EloquentBuilder $query): EloquentBuilder => $translatableContentDriver->applySearchConstraintToQuery($query, $searchColumn, $search, $whereClause, $isSearchForcedCaseInsensitive), fn (EloquentBuilder $query) => $query->when( $this->hasRelationship($query->getModel()), - fn (EloquentBuilder $query): EloquentBuilder => $query->{"{$whereClause}Relation"}( - $this->getRelationshipName($query->getModel()), - generate_search_column_expression((string) str($searchColumn)->replace('.', '->'), $isSearchForcedCaseInsensitive, $databaseConnection), - 'like', - "%{$nonTranslatableSearch}%", - ), + function (EloquentBuilder $query) use ($model, $whereClause, $searchColumn, $isSearchForcedCaseInsensitive, $databaseConnection, $nonTranslatableSearch): EloquentBuilder { + $relationshipName = $this->getRelationshipName($query->getModel()); + $relationship = $this->getRelationship($query->getModel(), $relationshipName); + + $relatedTable = $model->getTable(); + + if ($relationship instanceof BelongsToThrough) { + $relatedTable = $relationship->getRelated()->getTable(); + $searchColumn = str($searchColumn)->startsWith("{$relatedTable}.") + ? $searchColumn + : $relationship->getRelated()->qualifyColumn($searchColumn); + } + + return $query->{"{$whereClause}Relation"}( + $relationshipName, + generate_search_column_expression($this->getJsonSafeColumnName($searchColumn, $relatedTable), $isSearchForcedCaseInsensitive, $databaseConnection), + 'like', + "%{$nonTranslatableSearch}%", + ); + }, fn (EloquentBuilder $query) => $query->{$whereClause}( - generate_search_column_expression((string) str($searchColumn)->replace('.', '->'), $isSearchForcedCaseInsensitive, $databaseConnection), + generate_search_column_expression($this->getJsonSafeColumnName($searchColumn, $model->getTable()), $isSearchForcedCaseInsensitive, $databaseConnection), 'like', "%{$nonTranslatableSearch}%", ), @@ -107,6 +123,15 @@ trait InteractsWithTableQuery return $query; } + protected function getJsonSafeColumnName(string $column, string $tableName): string + { + if (str($column)->startsWith("{$tableName}.")) { + return (string) str($column)->after('.')->replace('.', '->')->prepend("{$tableName}."); + } + + return (string) str($column)->replace('.', '->'); + } + public function applySort(EloquentBuilder $query, string $direction = 'asc'): EloquentBuilder { if ($this->sortQuery) { @@ -121,10 +146,13 @@ trait InteractsWithTableQuery $relationshipName = $this->getRelationshipName($query->getModel()); foreach (array_reverse($this->getSortColumns($query->getModel())) as $sortColumn) { - $sortColumn = (string) str($sortColumn)->replace('.', '->'); + $sortColumn = $this->getJsonSafeColumnName($sortColumn, $query->getModel()->getTable()); - if ($relationshipName) { - $query->orderByPowerJoins("{$relationshipName}.{$sortColumn}", $direction, joinType: 'leftJoin'); /** @phpstan-ignore method.notFound */ + if (filled($relationshipName)) { + $query->orderBy( + app(RelationshipOrderer::class)->buildSubquery($query, $relationshipName, $sortColumn), + $direction + ); continue; } diff --git a/packages/tables/src/Columns/Summarizers/Summarizer.php b/packages/tables/src/Columns/Summarizers/Summarizer.php index 4441418d40..27a452764e 100644 --- a/packages/tables/src/Columns/Summarizers/Summarizer.php +++ b/packages/tables/src/Columns/Summarizers/Summarizer.php @@ -79,7 +79,17 @@ class Summarizer extends ViewComponent implements HasEmbeddedView $attribute = $column->getName(); $query = $this->getQuery()?->clone(); - if ($query && $column->hasRelationship($query->getModel())) { + $hasRelationship = $query && $column->hasRelationship($query->getModel()); + + if ($this->hasQueryModification() && $hasRelationship) { + $baseQueryForModification = $query->toBase(); + $this->evaluate($this->modifyQueryUsing, [ + 'attribute' => $attribute, + 'query' => $baseQueryForModification, + ]); + } + + if ($hasRelationship) { $relationship = $column->getRelationship($query->getModel()); $attribute = $column->getFullAttributeName($query->getModel()); @@ -129,7 +139,7 @@ class Summarizer extends ViewComponent implements HasEmbeddedView $query = $query?->getModel()->resolveConnection($query->getModel()->getConnectionName()) ->table($query->toBase(), $asName); - if ($this->hasQueryModification()) { + if ($this->hasQueryModification() && ! $hasRelationship) { $query = $this->evaluate($this->modifyQueryUsing, [ 'attribute' => $attribute, 'query' => $query, diff --git a/packages/tables/src/Grouping/Group.php b/packages/tables/src/Grouping/Group.php index 8a5e75ebd1..a5e961f46c 100644 --- a/packages/tables/src/Grouping/Group.php +++ b/packages/tables/src/Grouping/Group.php @@ -8,6 +8,7 @@ use Carbon\CarbonInterface; use Closure; use Filament\Support\Components\Component; use Filament\Support\Contracts\HasLabel as LabelInterface; +use Filament\Support\Services\RelationshipOrderer; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; @@ -321,8 +322,11 @@ class Group extends Component ]) ?? $query; } - if ($relationshipName = $this->getRelationshipName()) { - return $query->orderByPowerJoins("{$relationshipName}.{$this->getRelationshipAttribute()}", $direction, joinType: 'leftJoin'); /** @phpstan-ignore method.notFound */ + if (filled($relationshipName = $this->getRelationshipName())) { + return $query->orderBy( + app(RelationshipOrderer::class)->buildSubquery($query, $relationshipName, $this->getRelationshipAttribute()), + $direction + ); } return $query->orderBy($this->getRelationshipAttribute(), $direction); diff --git a/packages/tables/src/Testing/TestsSummaries.php b/packages/tables/src/Testing/TestsSummaries.php index 8a4427b7e3..f3e381b219 100644 --- a/packages/tables/src/Testing/TestsSummaries.php +++ b/packages/tables/src/Testing/TestsSummaries.php @@ -23,7 +23,7 @@ class TestsSummaries $this->assertTableColumnSummarizerExists($columnName, $summarizerId); $normalizeState = fn ($state): string => strval( - is_numeric($state) ? round(floatval($state), 5) : $state, + is_numeric($state) ? round(floatval($state), 4) : $state, ); $state = is_array($state) ? array_map($normalizeState, $state) : $normalizeState($state); @@ -57,7 +57,7 @@ class TestsSummaries $this->assertTableColumnSummarizerExists($columnName, $summarizerId); $normalizeState = fn ($state): string => strval( - is_numeric($state) ? round(floatval($state), 5) : $state, + is_numeric($state) ? round(floatval($state), 4) : $state, ); $state = is_array($state) ? array_map($normalizeState, $state) : $normalizeState($state); diff --git a/phpunit.mysql.xml b/phpunit.mysql.xml new file mode 100644 index 0000000000..43d7128dad --- /dev/null +++ b/phpunit.mysql.xml @@ -0,0 +1,16 @@ + + + + + ./tests/ + + + + + + + + + + + diff --git a/phpunit.pgsql.xml b/phpunit.pgsql.xml new file mode 100644 index 0000000000..48811c6c50 --- /dev/null +++ b/phpunit.pgsql.xml @@ -0,0 +1,16 @@ + + + + + ./tests/ + + + + + + + + + + + diff --git a/phpunit.sqlite.xml b/phpunit.sqlite.xml new file mode 100644 index 0000000000..cfb81d804e --- /dev/null +++ b/phpunit.sqlite.xml @@ -0,0 +1,16 @@ + + + + + ./tests/ + + + + + + + + + + + diff --git a/tests/.pest/snapshots/src/Forms/Commands/MakeFormCommandTest/it_can_generate_a_form_schema_class_for_a_model.snap b/tests/.pest/snapshots/src/Forms/Commands/MakeFormCommandTest/it_can_generate_a_form_schema_class_for_a_model.snap index 52f32cf7e6..e42036ffff 100644 --- a/tests/.pest/snapshots/src/Forms/Commands/MakeFormCommandTest/it_can_generate_a_form_schema_class_for_a_model.snap +++ b/tests/.pest/snapshots/src/Forms/Commands/MakeFormCommandTest/it_can_generate_a_form_schema_class_for_a_model.snap @@ -17,8 +17,7 @@ class PostForm return $schema ->components([ Select::make('author_id') - ->relationship('author', 'name') - ->required(), + ->relationship('author', 'name'), Textarea::make('content') ->columnSpanFull(), Toggle::make('is_published') @@ -31,6 +30,7 @@ class PostForm ->columnSpanFull(), TextInput::make('title') ->required(), + TextInput::make('title2'), Textarea::make('config') ->columnSpanFull(), Textarea::make('json') diff --git a/tests/.pest/snapshots/src/Forms/Commands/MakeLivewireFormCommandTest/it_can_generate_a_livewire_form_component_with_generated_fields.snap b/tests/.pest/snapshots/src/Forms/Commands/MakeLivewireFormCommandTest/it_can_generate_a_livewire_form_component_with_generated_fields.snap index 86fd502ea1..f9744e4db7 100644 --- a/tests/.pest/snapshots/src/Forms/Commands/MakeLivewireFormCommandTest/it_can_generate_a_livewire_form_component_with_generated_fields.snap +++ b/tests/.pest/snapshots/src/Forms/Commands/MakeLivewireFormCommandTest/it_can_generate_a_livewire_form_component_with_generated_fields.snap @@ -33,8 +33,7 @@ class CreatePostWithFields extends Component implements HasActions, HasSchemas return $schema ->components([ Select::make('author_id') - ->relationship('author', 'name') - ->required(), + ->relationship('author', 'name'), Textarea::make('content') ->columnSpanFull(), Toggle::make('is_published') @@ -47,6 +46,7 @@ class CreatePostWithFields extends Component implements HasActions, HasSchemas ->columnSpanFull(), TextInput::make('title') ->required(), + TextInput::make('title2'), Textarea::make('config') ->columnSpanFull(), Textarea::make('json') diff --git a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap index 7c687f818a..e38f475ed1 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap @@ -32,6 +32,8 @@ class ManageUserTeams extends ManageRelatedRecords ->required() ->numeric() ->default(0), + Forms\Components\Select::make('company_id') + ->relationship('company', 'name'), ]); } @@ -45,6 +47,8 @@ class ManageUserTeams extends ManageRelatedRecords Tables\Columns\TextColumn::make('budget') ->numeric() ->sortable(), + Tables\Columns\TextColumn::make('company.name') + ->searchable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() diff --git a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap index 855db5068b..2f45f893e0 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap @@ -25,6 +25,8 @@ class TeamsRelationManager extends RelationManager ->required() ->numeric() ->default(0), + Forms\Components\Select::make('company_id') + ->relationship('company', 'name'), ]); } @@ -38,6 +40,8 @@ class TeamsRelationManager extends RelationManager Tables\Columns\TextColumn::make('budget') ->numeric() ->sortable(), + Tables\Columns\TextColumn::make('company.name') + ->searchable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() diff --git a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap index 9c003f64ae..8e8d27699f 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap @@ -25,8 +25,7 @@ class PostResource extends Resource return $schema ->components([ Forms\Components\Select::make('author_id') - ->relationship('author', 'name') - ->required(), + ->relationship('author', 'name'), Forms\Components\Textarea::make('content') ->columnSpanFull(), Forms\Components\Toggle::make('is_published') @@ -39,6 +38,7 @@ class PostResource extends Resource ->columnSpanFull(), Forms\Components\TextInput::make('title') ->required(), + Forms\Components\TextInput::make('title2'), Forms\Components\Textarea::make('config') ->columnSpanFull(), Forms\Components\Textarea::make('json') @@ -63,6 +63,8 @@ class PostResource extends Resource ->sortable(), Tables\Columns\TextColumn::make('title') ->searchable(), + Tables\Columns\TextColumn::make('title2') + ->searchable(), Tables\Columns\TextColumn::make('string_backed_enum') ->badge() ->searchable(), diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap b/tests/.pest/snapshots/src/Panels/Commands/MakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap index 5a0d37bd8a..f0437bd63b 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakePageCommandTest/it_can_generate_a_manage_related_records_page_class_in_a_resource_with_a_generated_form_schema_and_table_columns.snap @@ -12,6 +12,7 @@ use Filament\Actions\DeleteBulkAction; use Filament\Actions\DetachAction; use Filament\Actions\DetachBulkAction; use Filament\Actions\EditAction; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Textarea; use Filament\Resources\Pages\ManageRelatedRecords; @@ -40,6 +41,8 @@ class ManageUserTeams extends ManageRelatedRecords ->required() ->numeric() ->default(0), + Select::make('company_id') + ->relationship('company', 'name'), ]); } @@ -53,6 +56,8 @@ class ManageUserTeams extends ManageRelatedRecords TextColumn::make('budget') ->numeric() ->sortable(), + TextColumn::make('company.name') + ->searchable(), TextColumn::make('created_at') ->dateTime() ->sortable() diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap index e0f70a1a36..6001a9b25a 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeRelationManagerCommandTest/it_can_generate_a_relation_manager_with_a_generated_form_schema_and_table_columns.snap @@ -10,6 +10,7 @@ use Filament\Actions\DeleteBulkAction; use Filament\Actions\DetachAction; use Filament\Actions\DetachBulkAction; use Filament\Actions\EditAction; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Textarea; use Filament\Resources\RelationManagers\RelationManager; @@ -33,6 +34,8 @@ class TeamsRelationManager extends RelationManager ->required() ->numeric() ->default(0), + Select::make('company_id') + ->relationship('company', 'name'), ]); } @@ -46,6 +49,8 @@ class TeamsRelationManager extends RelationManager TextColumn::make('budget') ->numeric() ->sortable(), + TextColumn::make('company.name') + ->searchable(), TextColumn::make('created_at') ->dateTime() ->sortable() diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form__infolist__and_table_content_embedded_in_a_resource_class.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form__infolist__and_table_content_embedded_in_a_resource_class.snap index f75377a26c..d0e01355a4 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form__infolist__and_table_content_embedded_in_a_resource_class.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form__infolist__and_table_content_embedded_in_a_resource_class.snap @@ -37,8 +37,7 @@ class PostResource extends Resource return $schema ->components([ Select::make('author_id') - ->relationship('author', 'name') - ->required(), + ->relationship('author', 'name'), Textarea::make('content') ->columnSpanFull(), Toggle::make('is_published') @@ -51,6 +50,7 @@ class PostResource extends Resource ->columnSpanFull(), TextInput::make('title') ->required(), + TextInput::make('title2'), Textarea::make('config') ->columnSpanFull(), Textarea::make('json') @@ -67,7 +67,8 @@ class PostResource extends Resource return $schema ->components([ TextEntry::make('author.name') - ->label('Author'), + ->label('Author') + ->placeholder('-'), TextEntry::make('content') ->placeholder('-') ->columnSpanFull(), @@ -79,6 +80,8 @@ class PostResource extends Resource ->placeholder('-') ->columnSpanFull(), TextEntry::make('title'), + TextEntry::make('title2') + ->placeholder('-'), TextEntry::make('config') ->placeholder('-') ->columnSpanFull(), @@ -116,6 +119,8 @@ class PostResource extends Resource ->sortable(), TextColumn::make('title') ->searchable(), + TextColumn::make('title2') + ->searchable(), TextColumn::make('string_backed_enum') ->badge() ->searchable(), diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap index aa555dc4e9..cb596071f9 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap @@ -16,8 +16,7 @@ class PostForm return $schema ->components([ Select::make('author_id') - ->relationship('author', 'name') - ->required(), + ->relationship('author', 'name'), Textarea::make('content') ->columnSpanFull(), Toggle::make('is_published') @@ -30,6 +29,7 @@ class PostForm ->columnSpanFull(), TextInput::make('title') ->required(), + TextInput::make('title2'), Textarea::make('config') ->columnSpanFull(), Textarea::make('json') diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_infolist_content.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_infolist_content.snap index ac35a13189..6eea1e8e4d 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_infolist_content.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_infolist_content.snap @@ -14,7 +14,8 @@ class PostInfolist return $schema ->components([ TextEntry::make('author.name') - ->label('Author'), + ->label('Author') + ->placeholder('-'), TextEntry::make('content') ->placeholder('-') ->columnSpanFull(), @@ -26,6 +27,8 @@ class PostInfolist ->placeholder('-') ->columnSpanFull(), TextEntry::make('title'), + TextEntry::make('title2') + ->placeholder('-'), TextEntry::make('config') ->placeholder('-') ->columnSpanFull(), diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_table_content.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_table_content.snap index 0929cde8f9..9cef924ca0 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_table_content.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_table_content.snap @@ -24,6 +24,8 @@ class PostsTable ->sortable(), TextColumn::make('title') ->searchable(), + TextColumn::make('title2') + ->searchable(), TextColumn::make('string_backed_enum') ->badge() ->searchable(), diff --git a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component__2.snap b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component__2.snap index c3fedc4aac..ee60d972b6 100644 --- a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component__2.snap +++ b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component__2.snap @@ -1,3 +1,51 @@ -
- {{ $this->table }} -
+query(fn (): Builder => BlogPost::query()) + ->columns([ + // + ]) + ->filters([ + // + ]) + ->headerActions([ + // + ]) + ->recordActions([ + // + ]) + ->toolbarActions([ + BulkActionGroup::make([ + // + ]), + ]); + } + + public function render(): View + { + return view('livewire.list-blog-posts'); + } +} diff --git a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_for_a_model_in_a_nested_directory__2.snap b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_for_a_model_in_a_nested_directory__2.snap index c3fedc4aac..88fb20b079 100644 --- a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_for_a_model_in_a_nested_directory__2.snap +++ b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_for_a_model_in_a_nested_directory__2.snap @@ -1,3 +1,51 @@ -
- {{ $this->table }} -
+query(fn (): Builder => Category::query()) + ->columns([ + // + ]) + ->filters([ + // + ]) + ->headerActions([ + // + ]) + ->recordActions([ + // + ]) + ->toolbarActions([ + BulkActionGroup::make([ + // + ]), + ]); + } + + public function render(): View + { + return view('livewire.blog.list-categories'); + } +} diff --git a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_in_a_nested_directory__2.snap b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_in_a_nested_directory__2.snap index c3fedc4aac..5a2a669542 100644 --- a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_in_a_nested_directory__2.snap +++ b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_in_a_nested_directory__2.snap @@ -1,3 +1,51 @@ -
- {{ $this->table }} -
+query(fn (): Builder => Post::query()) + ->columns([ + // + ]) + ->filters([ + // + ]) + ->headerActions([ + // + ]) + ->recordActions([ + // + ]) + ->toolbarActions([ + BulkActionGroup::make([ + // + ]), + ]); + } + + public function render(): View + { + return view('livewire.blog.list-posts'); + } +} diff --git a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_a_model__2.snap b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_a_model__2.snap index c3fedc4aac..a0be87f571 100644 --- a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_a_model__2.snap +++ b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_a_model__2.snap @@ -1,3 +1,51 @@ -
- {{ $this->table }} -
+query(fn (): Builder => Post::query()) + ->columns([ + // + ]) + ->filters([ + // + ]) + ->headerActions([ + // + ]) + ->recordActions([ + // + ]) + ->toolbarActions([ + BulkActionGroup::make([ + // + ]), + ]); + } + + public function render(): View + { + return view('livewire.list-posts'); + } +} diff --git a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns.snap b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns.snap index 7e0f361f1b..7c331d4c85 100644 --- a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns.snap +++ b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns.snap @@ -37,6 +37,8 @@ class ListPostsWithColumns extends Component implements HasActions, HasSchemas, ->sortable(), TextColumn::make('title') ->searchable(), + TextColumn::make('title2') + ->searchable(), TextColumn::make('string_backed_enum') ->badge() ->searchable(), diff --git a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns__2.snap b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns__2.snap index c3fedc4aac..7c331d4c85 100644 --- a/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns__2.snap +++ b/tests/.pest/snapshots/src/Tables/Commands/MakeLivewireTableCommandTest/it_can_generate_a_Livewire_table_component_with_generated_columns__2.snap @@ -1,3 +1,78 @@ -
- {{ $this->table }} -
+query(fn (): Builder => Post::query()) + ->columns([ + TextColumn::make('author.name') + ->searchable(), + IconColumn::make('is_published') + ->boolean(), + TextColumn::make('rating') + ->numeric() + ->sortable(), + TextColumn::make('title') + ->searchable(), + TextColumn::make('title2') + ->searchable(), + TextColumn::make('string_backed_enum') + ->badge() + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('deleted_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->headerActions([ + // + ]) + ->recordActions([ + // + ]) + ->toolbarActions([ + BulkActionGroup::make([ + // + ]), + ]); + } + + public function render(): View + { + return view('livewire.list-posts-with-columns'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000000..009c9bc066 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,12 @@ +safeLoad(); +} diff --git a/tests/database/factories/CompanyFactory.php b/tests/database/factories/CompanyFactory.php new file mode 100644 index 0000000000..c9cebb2858 --- /dev/null +++ b/tests/database/factories/CompanyFactory.php @@ -0,0 +1,18 @@ + $this->faker->company(), + ]; + } +} diff --git a/tests/database/factories/ImageFactory.php b/tests/database/factories/ImageFactory.php new file mode 100644 index 0000000000..7971dd7c3b --- /dev/null +++ b/tests/database/factories/ImageFactory.php @@ -0,0 +1,19 @@ + $this->faker->imageUrl(), + 'alt_text' => $this->faker->sentence(), + ]; + } +} diff --git a/tests/database/factories/ProfileFactory.php b/tests/database/factories/ProfileFactory.php new file mode 100644 index 0000000000..8d797b6800 --- /dev/null +++ b/tests/database/factories/ProfileFactory.php @@ -0,0 +1,22 @@ + User::factory(), + 'company_id' => Company::factory(), + 'bio' => $this->faker->sentence(), + ]; + } +} diff --git a/tests/database/factories/SettingFactory.php b/tests/database/factories/SettingFactory.php new file mode 100644 index 0000000000..32b9419d4c --- /dev/null +++ b/tests/database/factories/SettingFactory.php @@ -0,0 +1,21 @@ + Profile::factory(), + 'theme' => $this->faker->randomElement(['light', 'dark', 'auto']), + 'language' => $this->faker->randomElement(['en', 'es', 'fr', 'de']), + ]; + } +} diff --git a/tests/database/migrations/create_teams_table.php b/tests/database/migrations/0001_create_teams_table.php similarity index 91% rename from tests/database/migrations/create_teams_table.php rename to tests/database/migrations/0001_create_teams_table.php index 2049d5d5e0..05d54ffc18 100644 --- a/tests/database/migrations/create_teams_table.php +++ b/tests/database/migrations/0001_create_teams_table.php @@ -13,6 +13,7 @@ return new class extends Migration $table->string('name'); $table->text('description')->nullable(); $table->decimal('budget', 10, 2)->default(0); + $table->foreignId('company_id')->nullable(); $table->timestamps(); }); } diff --git a/tests/database/migrations/0002_create_companies_table.php b/tests/database/migrations/0002_create_companies_table.php new file mode 100644 index 0000000000..3a0213162b --- /dev/null +++ b/tests/database/migrations/0002_create_companies_table.php @@ -0,0 +1,22 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('companies'); + } +}; diff --git a/tests/database/migrations/create_departments_table.php b/tests/database/migrations/0003_create_departments_table.php similarity index 100% rename from tests/database/migrations/create_departments_table.php rename to tests/database/migrations/0003_create_departments_table.php diff --git a/tests/database/migrations/create_posts_table.php b/tests/database/migrations/0004_create_posts_table.php similarity index 89% rename from tests/database/migrations/create_posts_table.php rename to tests/database/migrations/0004_create_posts_table.php index ce9592a29c..e88933d48c 100644 --- a/tests/database/migrations/create_posts_table.php +++ b/tests/database/migrations/0004_create_posts_table.php @@ -10,12 +10,13 @@ return new class extends Migration { Schema::create('posts', function (Blueprint $table): void { $table->id(); - $table->foreignId('author_id'); + $table->foreignId('author_id')->nullable(); $table->text('content')->nullable(); $table->boolean('is_published')->default(true); $table->unsignedTinyInteger('rating')->default(0); $table->json('tags')->nullable(); $table->string('title'); + $table->string('title2')->nullable(); $table->json('config')->nullable(); $table->json('json')->nullable(); $table->json('json_array_of_objects')->nullable(); diff --git a/tests/database/migrations/0005_create_profiles_table.php b/tests/database/migrations/0005_create_profiles_table.php new file mode 100644 index 0000000000..4e3e7c68f6 --- /dev/null +++ b/tests/database/migrations/0005_create_profiles_table.php @@ -0,0 +1,24 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('company_id')->nullable()->constrained()->nullOnDelete(); + $table->string('bio')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('profiles'); + } +}; diff --git a/tests/database/migrations/0006_create_settings_table.php b/tests/database/migrations/0006_create_settings_table.php new file mode 100644 index 0000000000..df2dd30190 --- /dev/null +++ b/tests/database/migrations/0006_create_settings_table.php @@ -0,0 +1,24 @@ +id(); + $table->foreignId('profile_id')->constrained()->cascadeOnDelete(); + $table->string('theme')->default('light'); + $table->string('language')->default('en'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('settings'); + } +}; diff --git a/tests/database/migrations/0007_create_images_table.php b/tests/database/migrations/0007_create_images_table.php new file mode 100644 index 0000000000..7f013a54c2 --- /dev/null +++ b/tests/database/migrations/0007_create_images_table.php @@ -0,0 +1,24 @@ +id(); + $table->morphs('imageable'); + $table->string('url'); + $table->string('alt_text')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('images'); + } +}; diff --git a/tests/database/migrations/create_tickets_table.php b/tests/database/migrations/0008_create_tickets_table.php similarity index 100% rename from tests/database/migrations/create_tickets_table.php rename to tests/database/migrations/0008_create_tickets_table.php diff --git a/tests/database/migrations/modify_users_table.php b/tests/database/migrations/0009_modify_users_table.php similarity index 93% rename from tests/database/migrations/modify_users_table.php rename to tests/database/migrations/0009_modify_users_table.php index 86f0a80342..8c519c0269 100644 --- a/tests/database/migrations/modify_users_table.php +++ b/tests/database/migrations/0009_modify_users_table.php @@ -15,7 +15,7 @@ return new class extends Migration $table->after('password', function (Blueprint $table): void { $table->json('json')->nullable(); $table->boolean('has_email_authentication')->default(false); - $table->string('app_authentication_secret')->nullable(); + $table->text('app_authentication_secret')->nullable(); $table->text('app_authentication_recovery_codes')->nullable(); }); }); diff --git a/tests/database/migrations/0010_add_team_id_to_users_table.php b/tests/database/migrations/0010_add_team_id_to_users_table.php new file mode 100644 index 0000000000..b6ae709554 --- /dev/null +++ b/tests/database/migrations/0010_add_team_id_to_users_table.php @@ -0,0 +1,33 @@ +foreignId('team_id')->nullable()->constrained('teams')->nullOnDelete(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + if (Schema::hasColumn('users', 'team_id')) { + $table->dropForeign(['team_id']); + $table->dropColumn('team_id'); + } + }); + } +}; diff --git a/tests/database/migrations/create_team_user_table.php b/tests/database/migrations/0011_create_team_user_table.php similarity index 100% rename from tests/database/migrations/create_team_user_table.php rename to tests/database/migrations/0011_create_team_user_table.php diff --git a/tests/database/migrations/create_domain_teams_table.php b/tests/database/migrations/0012_create_domain_teams_table.php similarity index 100% rename from tests/database/migrations/create_domain_teams_table.php rename to tests/database/migrations/0012_create_domain_teams_table.php diff --git a/tests/database/migrations/create_department_ticket_table.php b/tests/database/migrations/0013_create_department_ticket_table.php similarity index 100% rename from tests/database/migrations/create_department_ticket_table.php rename to tests/database/migrations/0013_create_department_ticket_table.php diff --git a/tests/database/migrations/create_ticket_messages_table.php b/tests/database/migrations/0014_create_ticket_messages_table.php similarity index 100% rename from tests/database/migrations/create_ticket_messages_table.php rename to tests/database/migrations/0014_create_ticket_messages_table.php diff --git a/tests/src/Fixtures/Livewire/PostsTable.php b/tests/src/Fixtures/Livewire/PostsTable.php index 3665da4939..05070c0914 100644 --- a/tests/src/Fixtures/Livewire/PostsTable.php +++ b/tests/src/Fixtures/Livewire/PostsTable.php @@ -39,8 +39,14 @@ class PostsTable extends Component implements HasActions, HasSchemas, Tables\Con return $table ->query(Post::query()) ->groups(fn () => [ + Tables\Grouping\Group::make('title'), Tables\Grouping\Group::make('author.name') ->label(fn (Table $table, self $livewire) => 'Dynamic label'), + Tables\Grouping\Group::make('author.team.name'), + Tables\Grouping\Group::make('author.profile.bio'), + Tables\Grouping\Group::make('author.profile.company.name'), + Tables\Grouping\Group::make('author.image.url'), + Tables\Grouping\Group::make('author.profile.image.alt_text'), ]) ->columns([ Tables\Columns\TextColumn::make('title') @@ -64,6 +70,37 @@ class PostsTable extends Component implements HasActions, HasSchemas, Tables\Con ]), Tables\Columns\TextColumn::make('author.email') ->searchable(isIndividual: true, isGlobal: false), + Tables\Columns\TextColumn::make('author.team.name') + ->label('Author Team') + ->sortable(), + Tables\Columns\TextColumn::make('author.company.name') + ->label('Author Company (BelongsTo -> BelongsToThrough)') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('team.name') + ->label('Team (BelongsToThrough)') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('team.company.name') + ->label('Team Company (Nested BelongsToThrough)') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('author.profile.bio') + ->label('Author Profile Bio') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('author.profile.company.name') + ->label('Author Company') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('author.image.url') + ->label('Author Image URL') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('author.profile.image.alt_text') + ->label('Profile Image Alt') + ->sortable() + ->searchable(), Tables\Columns\IconColumn::make('is_published') ->boolean() ->summarize([ diff --git a/tests/src/Fixtures/Livewire/PostsTableWithQualifiedColumns.php b/tests/src/Fixtures/Livewire/PostsTableWithQualifiedColumns.php new file mode 100644 index 0000000000..7a409c6871 --- /dev/null +++ b/tests/src/Fixtures/Livewire/PostsTableWithQualifiedColumns.php @@ -0,0 +1,44 @@ +query(Post::query()) + ->columns([ + Tables\Columns\TextColumn::make('id') + ->sortable() + ->searchable(['posts.id']), + Tables\Columns\TextColumn::make('title') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('author.team.name') + ->label('Author Team') + ->sortable() + ->searchable(), + ]) + ->paginated(false); + } + + public function render(): View + { + return view('livewire.table'); + } +} diff --git a/tests/src/Fixtures/Livewire/UsersTable.php b/tests/src/Fixtures/Livewire/UsersTable.php new file mode 100644 index 0000000000..45664dd6fc --- /dev/null +++ b/tests/src/Fixtures/Livewire/UsersTable.php @@ -0,0 +1,65 @@ +query(User::query()) + ->groups(fn () => [ + Tables\Grouping\Group::make('name'), + Tables\Grouping\Group::make('profile.company.name'), + Tables\Grouping\Group::make('profile.setting.theme'), + Tables\Grouping\Group::make('image.url'), + ]) + ->columns([ + Tables\Columns\TextColumn::make('name') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('email') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('profile.bio') + ->label('Profile Bio') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('profile.company.name') + ->label('Company') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('profile.setting.theme') + ->label('Theme') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('profile.setting.language') + ->label('Language') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('image.url') + ->label('Image URL') + ->sortable() + ->searchable(), + ]); + } + + public function render(): View + { + return view('livewire.table'); + } +} diff --git a/tests/src/Fixtures/Livewire/UsersWithTeamTable.php b/tests/src/Fixtures/Livewire/UsersWithTeamTable.php new file mode 100644 index 0000000000..826b471d32 --- /dev/null +++ b/tests/src/Fixtures/Livewire/UsersWithTeamTable.php @@ -0,0 +1,45 @@ +query(User::query()) + ->columns([ + Tables\Columns\TextColumn::make('id') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('name') + ->label('Name') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('team.name') + ->label('Team Name') + ->sortable() + ->searchable(), + ]) + ->paginated(false); + } + + public function render(): View + { + return view('livewire.table'); + } +} diff --git a/tests/src/Fixtures/Models/Company.php b/tests/src/Fixtures/Models/Company.php new file mode 100644 index 0000000000..3b34d39736 --- /dev/null +++ b/tests/src/Fixtures/Models/Company.php @@ -0,0 +1,19 @@ +morphTo(); + } + + protected static function newFactory() + { + return ImageFactory::new(); + } +} diff --git a/tests/src/Fixtures/Models/Post.php b/tests/src/Fixtures/Models/Post.php index 6394aacc9b..d9e15d3aa3 100644 --- a/tests/src/Fixtures/Models/Post.php +++ b/tests/src/Fixtures/Models/Post.php @@ -8,9 +8,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Znck\Eloquent\Relations\BelongsToThrough; +use Znck\Eloquent\Traits\BelongsToThrough as BelongsToThroughTrait; class Post extends Model { + use BelongsToThroughTrait; use HasFactory; use SoftDeletes; @@ -36,6 +39,15 @@ class Post extends Model return $this->belongsTo(User::class, 'author_id'); } + public function team(): BelongsToThrough + { + return $this->belongsToThrough( + Team::class, + User::class, + foreignKeyLookup: [User::class => 'author_id'] + ); + } + public function config(string $key): mixed { return $this->config[$key] ?? null; diff --git a/tests/src/Fixtures/Models/Profile.php b/tests/src/Fixtures/Models/Profile.php new file mode 100644 index 0000000000..a8c42b8352 --- /dev/null +++ b/tests/src/Fixtures/Models/Profile.php @@ -0,0 +1,42 @@ +belongsTo(User::class); + } + + public function company(): BelongsTo + { + return $this->belongsTo(Company::class); + } + + public function setting(): HasOne + { + return $this->hasOne(Setting::class); + } + + public function image(): MorphOne + { + return $this->morphOne(Image::class, 'imageable'); + } + + protected static function newFactory() + { + return ProfileFactory::new(); + } +} diff --git a/tests/src/Fixtures/Models/Setting.php b/tests/src/Fixtures/Models/Setting.php new file mode 100644 index 0000000000..48fb5992c4 --- /dev/null +++ b/tests/src/Fixtures/Models/Setting.php @@ -0,0 +1,25 @@ +belongsTo(Profile::class); + } + + protected static function newFactory() + { + return SettingFactory::new(); + } +} diff --git a/tests/src/Fixtures/Models/Team.php b/tests/src/Fixtures/Models/Team.php index 9a3c2d971c..83a922869b 100644 --- a/tests/src/Fixtures/Models/Team.php +++ b/tests/src/Fixtures/Models/Team.php @@ -5,6 +5,7 @@ namespace Filament\Tests\Fixtures\Models; use Filament\Tests\Database\Factories\TeamFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class Team extends Model { @@ -12,6 +13,11 @@ class Team extends Model protected $guarded = []; + public function company(): BelongsTo + { + return $this->belongsTo(Company::class); + } + protected static function newFactory() { return TeamFactory::new(); diff --git a/tests/src/Fixtures/Models/User.php b/tests/src/Fixtures/Models/User.php index 89aa6a4837..426b25fdec 100644 --- a/tests/src/Fixtures/Models/User.php +++ b/tests/src/Fixtures/Models/User.php @@ -12,14 +12,19 @@ use Filament\Tests\Database\Factories\UserFactory; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; +use Znck\Eloquent\Traits\BelongsToThrough as BelongsToThroughTrait; class User extends Authenticatable implements FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasEmailAuthentication, HasTenants, MustVerifyEmail { + use BelongsToThroughTrait; use HasFactory; use Notifiable; @@ -109,8 +114,28 @@ class User extends Authenticatable implements FilamentUser, HasAppAuthentication $this->save(); } + public function team(): BelongsTo + { + return $this->belongsTo(Team::class); + } + public function teams(): BelongsToMany { return $this->belongsToMany(Team::class); } + + public function profile(): HasOne + { + return $this->hasOne(Profile::class); + } + + public function image(): MorphOne + { + return $this->morphOne(Image::class, 'imageable'); + } + + public function company() + { + return $this->belongsToThrough(Company::class, Team::class); + } } diff --git a/tests/src/Forms/Commands/MakeFormCommandTest.php b/tests/src/Forms/Commands/MakeFormCommandTest.php index 02e7599f9a..b06f4e407f 100644 --- a/tests/src/Forms/Commands/MakeFormCommandTest.php +++ b/tests/src/Forms/Commands/MakeFormCommandTest.php @@ -32,8 +32,11 @@ it('can generate a form schema class for a model', function (): void { ]); assertFileExists($path = app_path('Filament/Schemas/PostForm.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a form schema class in a nested directory', function (): void { @@ -56,6 +59,9 @@ it('can generate a form schema class for a model in a nested directory', functio ]); assertFileExists($path = app_path('Filament/Schemas/Blog/CategoryForm.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); diff --git a/tests/src/Forms/Commands/MakeLivewireFormCommandTest.php b/tests/src/Forms/Commands/MakeLivewireFormCommandTest.php index 782f7199a9..700c37d04c 100644 --- a/tests/src/Forms/Commands/MakeLivewireFormCommandTest.php +++ b/tests/src/Forms/Commands/MakeLivewireFormCommandTest.php @@ -72,12 +72,16 @@ it('can generate a Livewire form component with generated fields', function (): ]); assertFileExists($path = app_path('Livewire/CreatePostWithFields.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileExists($viewPath = resource_path('views/livewire/create-post-with-fields.blade.php')); - expect(file_get_contents($viewPath)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($viewPath)) + ->toMatchSnapshot(); + } }); it('can generate a Livewire form component in a nested directory', function (): void { diff --git a/tests/src/Panels/Commands/Legacy/LegacyMakePageCommandTest.php b/tests/src/Panels/Commands/Legacy/LegacyMakePageCommandTest.php index 58904fbfd9..08b6e418c3 100644 --- a/tests/src/Panels/Commands/Legacy/LegacyMakePageCommandTest.php +++ b/tests/src/Panels/Commands/Legacy/LegacyMakePageCommandTest.php @@ -407,8 +407,10 @@ it('can generate a manage related records page class in a resource with a genera ->expectsQuestion($questions['relationshipType'], BelongsToMany::class); assertFileExists($path = app_path('Filament/Resources/UserResource/Pages/ManageUserTeams.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a manage related records page class in a resource with a view operation', function () use ($runGenerateManageRelatedRecordsPageCommand, $generateManageRelatedRecordsPageCommandQuestions): void { diff --git a/tests/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest.php b/tests/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest.php index 4687201c27..c969c97740 100644 --- a/tests/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest.php +++ b/tests/src/Panels/Commands/Legacy/LegacyMakeRelationManagerCommandTest.php @@ -97,8 +97,10 @@ it('can generate a relation manager with a generated form schema and table colum ]); assertFileExists($path = app_path('Filament/Resources/UserResource/RelationManagers/TeamsRelationManager.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a relation manager with a view operation', function (): void { diff --git a/tests/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest.php b/tests/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest.php index bb36256864..149da79f29 100644 --- a/tests/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest.php +++ b/tests/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest.php @@ -33,8 +33,11 @@ it('can generate a resource class', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource list page', function (): void { @@ -46,8 +49,11 @@ it('can generate a resource list page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/ListPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource create page', function (): void { @@ -59,8 +65,11 @@ it('can generate a resource create page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/CreatePost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource edit page', function (): void { @@ -72,8 +81,11 @@ it('can generate a resource edit page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource view page', function (): void { @@ -86,8 +98,11 @@ it('can generate a resource view page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/ViewPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate the form and table of a resource class', function (): void { @@ -100,8 +115,11 @@ it('can generate the form and table of a resource class', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class with soft-deletes', function (): void { @@ -114,8 +132,11 @@ it('can generate a resource class with soft-deletes', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource edit page with soft-deletes', function (): void { @@ -128,8 +149,11 @@ it('can generate a resource edit page with soft-deletes', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a simple resource class', function (): void { @@ -142,8 +166,11 @@ it('can generate a simple resource class', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a simple resource manage page', function (): void { @@ -156,8 +183,11 @@ it('can generate a simple resource manage page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/ManagePosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class in a nested directory', function (): void { @@ -169,8 +199,11 @@ it('can generate a resource class in a nested directory', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Blog/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource list page in a nested directory', function (): void { @@ -182,8 +215,11 @@ it('can generate a resource list page in a nested directory', function (): void ]); assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/ListPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource create page in a nested directory', function (): void { @@ -195,8 +231,11 @@ it('can generate a resource create page in a nested directory', function (): voi ]); assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/CreatePost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource edit page in a nested directory', function (): void { @@ -208,8 +247,11 @@ it('can generate a resource edit page in a nested directory', function (): void ]); assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource view page in a nested directory', function (): void { @@ -222,8 +264,11 @@ it('can generate a resource view page in a nested directory', function (): void ]); assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/ViewPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a simple resource manage page in a nested directory', function (): void { @@ -236,6 +281,9 @@ it('can generate a simple resource manage page in a nested directory', function ]); assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/ManagePosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); diff --git a/tests/src/Panels/Commands/MakePageCommandTest.php b/tests/src/Panels/Commands/MakePageCommandTest.php index 1c5a803d71..9c75c89884 100644 --- a/tests/src/Panels/Commands/MakePageCommandTest.php +++ b/tests/src/Panels/Commands/MakePageCommandTest.php @@ -425,8 +425,10 @@ it('can generate a manage related records page class in a resource with a genera ->expectsQuestion($questions['relationshipType'], BelongsToMany::class); assertFileExists($path = app_path('Filament/Resources/Users/Pages/ManageUserTeams.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a manage related records page class in a resource with a view operation', function () use ($runGenerateManageRelatedRecordsPageCommand, $generateManageRelatedRecordsPageCommandQuestions): void { diff --git a/tests/src/Panels/Commands/MakeRelationManagerCommandTest.php b/tests/src/Panels/Commands/MakeRelationManagerCommandTest.php index 774f327afc..7cf495f035 100644 --- a/tests/src/Panels/Commands/MakeRelationManagerCommandTest.php +++ b/tests/src/Panels/Commands/MakeRelationManagerCommandTest.php @@ -108,8 +108,10 @@ it('can generate a relation manager with a generated form schema and table colum ]); assertFileExists($path = app_path('Filament/Resources/Users/RelationManagers/TeamsRelationManager.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a relation manager with a view operation', function (): void { diff --git a/tests/src/Panels/Commands/MakeResourceCommandTest.php b/tests/src/Panels/Commands/MakeResourceCommandTest.php index ab8e39b0eb..55555c9eb3 100644 --- a/tests/src/Panels/Commands/MakeResourceCommandTest.php +++ b/tests/src/Panels/Commands/MakeResourceCommandTest.php @@ -26,8 +26,11 @@ it('can generate a resource class', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class with a record title attribute', function (): void { @@ -40,8 +43,11 @@ it('can generate a resource class with a record title attribute', function (): v ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource form', function (): void { @@ -53,8 +59,11 @@ it('can generate a resource form', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostForm.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource infolist', function (): void { @@ -67,8 +76,11 @@ it('can generate a resource infolist', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostInfolist.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource table', function (): void { @@ -80,8 +92,11 @@ it('can generate a resource table', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Tables/PostsTable.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource list page', function (): void { @@ -93,8 +108,11 @@ it('can generate a resource list page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Pages/ListPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource create page', function (): void { @@ -106,8 +124,11 @@ it('can generate a resource create page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Pages/CreatePost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource edit page', function (): void { @@ -119,8 +140,11 @@ it('can generate a resource edit page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource view page', function (): void { @@ -133,8 +157,11 @@ it('can generate a resource view page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Pages/ViewPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class with embedded form', function (): void { @@ -147,8 +174,11 @@ it('can generate a resource class with embedded form', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class with embedded infolist', function (): void { @@ -162,8 +192,11 @@ it('can generate a resource class with embedded infolist', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class with embedded table', function (): void { @@ -176,8 +209,11 @@ it('can generate a resource class with embedded table', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate the resource form content', function (): void { @@ -190,8 +226,11 @@ it('can generate the resource form content', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostForm.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate the resource infolist content', function (): void { @@ -205,8 +244,11 @@ it('can generate the resource infolist content', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostInfolist.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate the resource table content', function (): void { @@ -219,8 +261,11 @@ it('can generate the resource table content', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Tables/PostsTable.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate the form, infolist, and table content embedded in a resource class', function (): void { @@ -236,8 +281,11 @@ it('can generate the form, infolist, and table content embedded in a resource cl ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class with soft-deletes', function (): void { @@ -250,8 +298,11 @@ it('can generate a resource class with soft-deletes', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource table with soft-deletes', function (): void { @@ -264,8 +315,11 @@ it('can generate a resource table with soft-deletes', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Tables/PostsTable.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource edit page with soft-deletes', function (): void { @@ -278,8 +332,11 @@ it('can generate a resource edit page with soft-deletes', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a simple resource class', function (): void { @@ -304,8 +361,11 @@ it('can generate a simple resource class', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileDoesNotExist(app_path('Filament/Resources/Posts/Schemas/PostForm.php')); assertFileDoesNotExist(app_path('Filament/Resources/Posts/Schemas/PostInfolist.php')); @@ -322,8 +382,11 @@ it('can generate a simple resource manage page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Posts/Pages/ManagePosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a simple resource class without embedded schemas and table', function (): void { @@ -338,8 +401,11 @@ it('can generate a simple resource class without embedded schemas and table', fu ]); assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource class in a nested directory', function (): void { @@ -351,8 +417,11 @@ it('can generate a resource class in a nested directory', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource form in a nested directory', function (): void { @@ -364,8 +433,11 @@ it('can generate a resource form in a nested directory', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Schemas/PostForm.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource infolist in a nested directory', function (): void { @@ -378,8 +450,11 @@ it('can generate a resource infolist in a nested directory', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Schemas/PostInfolist.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource table in a nested directory', function (): void { @@ -391,8 +466,11 @@ it('can generate a resource table in a nested directory', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Tables/PostsTable.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource list page in a nested directory', function (): void { @@ -404,8 +482,11 @@ it('can generate a resource list page in a nested directory', function (): void ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/ListPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource create page in a nested directory', function (): void { @@ -417,8 +498,11 @@ it('can generate a resource create page in a nested directory', function (): voi ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/CreatePost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource edit page in a nested directory', function (): void { @@ -430,8 +514,11 @@ it('can generate a resource edit page in a nested directory', function (): void ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a resource view page in a nested directory', function (): void { @@ -444,8 +531,11 @@ it('can generate a resource view page in a nested directory', function (): void ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/ViewPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a simple resource manage page in a nested directory', function (): void { @@ -458,8 +548,11 @@ it('can generate a simple resource manage page in a nested directory', function ]); assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/ManagePosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource class', function (): void { @@ -488,8 +581,11 @@ it('can generate a nested resource class', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource class with a plural parent resource name', function (): void { @@ -518,8 +614,11 @@ it('can generate a nested resource class with a plural parent resource name', fu ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource class with a parent resource name with `Resource` at the end', function (): void { @@ -548,8 +647,11 @@ it('can generate a nested resource class with a parent resource name with `Resou ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource form', function (): void { @@ -578,8 +680,11 @@ it('can generate a nested resource form', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Schemas/PostForm.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource infolist', function (): void { @@ -609,8 +714,11 @@ it('can generate a nested resource infolist', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Schemas/PostInfolist.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource create page', function (): void { @@ -639,8 +747,11 @@ it('can generate a nested resource create page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Pages/CreatePost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource edit page', function (): void { @@ -669,8 +780,11 @@ it('can generate a nested resource edit page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Pages/EditPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource view page', function (): void { @@ -700,8 +814,11 @@ it('can generate a nested resource view page', function (): void { ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Pages/ViewPost.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a nested resource class in a nested directory', function (): void { @@ -730,6 +847,9 @@ it('can generate a nested resource class in a nested directory', function (): vo ]); assertFileExists($path = app_path('Filament/Resources/Users/Resources/Blog/Posts/PostResource.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); diff --git a/tests/src/Panels/Commands/MakeSettingsPageCommandTest.php b/tests/src/Panels/Commands/MakeSettingsPageCommandTest.php index 0b666142c7..5f87a45b5f 100644 --- a/tests/src/Panels/Commands/MakeSettingsPageCommandTest.php +++ b/tests/src/Panels/Commands/MakeSettingsPageCommandTest.php @@ -79,8 +79,10 @@ it('can generate a page class with a generated form schema', function (): void { ]); assertFileExists($path = app_path('Filament/Pages/ManageSettings.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); class Settings extends BaseSettings diff --git a/tests/src/Panels/Resources/Pages/CreateRecordTest.php b/tests/src/Panels/Resources/Pages/CreateRecordTest.php index 4d81b7bf60..13d58579c0 100644 --- a/tests/src/Panels/Resources/Pages/CreateRecordTest.php +++ b/tests/src/Panels/Resources/Pages/CreateRecordTest.php @@ -43,13 +43,21 @@ it('can create', function (): void { ->assertHasNoFormErrors() ->assertRedirect(); - $this->assertDatabaseHas(Post::class, [ - 'author_id' => $newData->author->getKey(), - 'content' => $newData->content, - 'tags' => json_encode($newData->tags), - 'title' => $newData->title, - 'rating' => $newData->rating, - ]); + $record = Post::query() + + ->where('author_id', $newData->author->getKey()) + + ->where('content', $newData->content) + + ->where('title', $newData->title) + + ->where('rating', $newData->rating) + + ->first(); + + expect($record)->not->toBeNull(); + + expect($record->tags)->toBe($newData->tags); Event::assertDispatched(RecordCreated::class); Event::assertDispatched(RecordSaved::class); @@ -88,21 +96,31 @@ it('can create another', function (): void { ->assertHasNoFormErrors() ->assertRedirect(); - $this->assertDatabaseHas(Post::class, [ - 'author_id' => $newData->author->getKey(), - 'content' => $newData->content, - 'tags' => json_encode($newData->tags), - 'title' => $newData->title, - 'rating' => $newData->rating, - ]); + $record = Post::query() - $this->assertDatabaseHas(Post::class, [ - 'author_id' => $newData2->author->getKey(), - 'content' => $newData2->content, - 'tags' => json_encode($newData2->tags), - 'title' => $newData2->title, - 'rating' => $newData2->rating, - ]); + ->where('author_id', $newData->author->getKey()) + + ->where('content', $newData->content) + + ->where('title', $newData->title) + + ->where('rating', $newData->rating) + + ->first(); + + expect($record)->not->toBeNull(); + + expect($record->tags)->toBe($newData->tags); + + $record2 = Post::query() + ->where('author_id', $newData2->author->getKey()) + ->where('content', $newData2->content) + ->where('title', $newData2->title) + ->where('rating', $newData2->rating) + ->first(); + + expect($record2)->not->toBeNull(); + expect($record2->tags)->toBe($newData2->tags); }); it('can create another and preserve data', function (): void { @@ -136,21 +154,31 @@ it('can create another and preserve data', function (): void { ->assertHasNoFormErrors() ->assertRedirect(); - $this->assertDatabaseHas(Post::class, [ - 'author_id' => $newData->author->getKey(), - 'content' => $newData->content, - 'tags' => json_encode($newData->tags), - 'title' => $newData->title, - 'rating' => $newData->rating, - ]); + $record = Post::query() - $this->assertDatabaseHas(Post::class, [ - 'author_id' => $newData2->author->getKey(), - 'content' => $newData2->content, - 'tags' => json_encode($newData->tags), - 'title' => $newData2->title, - 'rating' => $newData->rating, - ]); + ->where('author_id', $newData->author->getKey()) + + ->where('content', $newData->content) + + ->where('title', $newData->title) + + ->where('rating', $newData->rating) + + ->first(); + + expect($record)->not->toBeNull(); + + expect($record->tags)->toBe($newData->tags); + + $record2 = Post::query() + ->where('author_id', $newData2->author->getKey()) + ->where('content', $newData2->content) + ->where('title', $newData2->title) + ->where('rating', $newData->rating) + ->first(); + + expect($record2)->not->toBeNull(); + expect($record2->tags)->toBe($newData->tags); }); it('can validate input', function (): void { diff --git a/tests/src/Panels/Resources/Pages/ListRecordsTest.php b/tests/src/Panels/Resources/Pages/ListRecordsTest.php index fc869f3582..89e33b9c3d 100644 --- a/tests/src/Panels/Resources/Pages/ListRecordsTest.php +++ b/tests/src/Panels/Resources/Pages/ListRecordsTest.php @@ -15,6 +15,7 @@ use Filament\Facades\Filament; use Filament\Tests\Fixtures\Models\Post; use Filament\Tests\Fixtures\Models\Ticket; use Filament\Tests\Fixtures\Models\TicketMessage; +use Filament\Tests\Fixtures\Models\User; use Filament\Tests\Fixtures\Policies\TicketPolicy; use Filament\Tests\Fixtures\Resources\Posts\Pages\ListPosts; use Filament\Tests\Fixtures\Resources\Posts\PostResource; @@ -63,40 +64,64 @@ it('can render post authors', function (): void { }); it('can sort posts by title', function (): void { - $posts = Post::factory()->count(10)->create(); + Post::factory()->count(10)->create(); + + $sortedAsc = Post::query()->orderBy('title')->get(); + $sortedDesc = Post::query()->orderByDesc('title')->get(); livewire(ListPosts::class) ->sortTable('title') - ->assertCanSeeTableRecords($posts->sortBy('title'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('title', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('title'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort posts by author', function (): void { - $posts = Post::factory()->count(10)->create(); + Post::factory()->count(10)->create(); + + $sortedAsc = Post::query() + ->orderBy( + User::query() + ->select('name') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + User::query() + ->select('name') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); livewire(ListPosts::class) ->sortTable('author.name') - ->assertCanSeeTableRecords($posts->sortBy('author.name'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.name', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('author.name'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort posts with default sort key', function (): void { $faker = fake()->unique(); - $posts = Post::factory()->count(10)->state(function () use ($faker) { + Post::factory()->count(10)->state(function () use ($faker) { return [ 'id' => $faker->randomDigit(), 'title' => 'Lorem Ipsum', ]; })->create(); + $sortedAsc = Post::query()->orderBy('title')->orderBy('id')->get(); + $sortedDesc = Post::query()->orderByDesc('title')->orderByDesc('id')->get(); + livewire(ListPosts::class) ->sortTable('title') - ->assertCanSeeTableRecords($posts->sortBy([['title', 'asc'], ['id', 'asc']]), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('title', 'desc') - ->assertCanSeeTableRecords($posts->sortBy([['title', 'desc'], ['id', 'desc']]), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can search posts by title', function (): void { diff --git a/tests/src/Panels/Tenancy/ResolveTenantTest.php b/tests/src/Panels/Tenancy/ResolveTenantTest.php index fdc2fa35cb..8ca5e66dfd 100644 --- a/tests/src/Panels/Tenancy/ResolveTenantTest.php +++ b/tests/src/Panels/Tenancy/ResolveTenantTest.php @@ -58,5 +58,5 @@ it('resolves the tenant with custom path correctly from the route', function (): Filament::setCurrentPanel($panel); Filament::setTenant($team); - expect(Filament::getUrl($team))->toBe('http://localhost/tenancy/1'); + expect(Filament::getUrl($team))->toBe('http://localhost/tenancy/' . $team->getKey()); }); diff --git a/tests/src/Tables/ColumnTest.php b/tests/src/Tables/ColumnTest.php index 961822fb7b..dcbb0631e9 100644 --- a/tests/src/Tables/ColumnTest.php +++ b/tests/src/Tables/ColumnTest.php @@ -3,7 +3,15 @@ use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tests\Fixtures\Livewire\PostsTable; +use Filament\Tests\Fixtures\Livewire\PostsTableWithQualifiedColumns; +use Filament\Tests\Fixtures\Livewire\UsersTable; +use Filament\Tests\Fixtures\Livewire\UsersWithTeamTable; +use Filament\Tests\Fixtures\Models\Company; +use Filament\Tests\Fixtures\Models\Image; use Filament\Tests\Fixtures\Models\Post; +use Filament\Tests\Fixtures\Models\Profile; +use Filament\Tests\Fixtures\Models\Setting; +use Filament\Tests\Fixtures\Models\Team; use Filament\Tests\Fixtures\Models\User; use Filament\Tests\Tables\TestCase; use Illuminate\Support\Str; @@ -27,23 +35,80 @@ it('can render text column with relationship', function (): void { }); it('can sort records', function (): void { - $posts = Post::factory()->count(10)->create(); + Post::factory()->count(10)->create(); + + $sortedAsc = Post::query()->orderBy('title')->get(); + $sortedDesc = Post::query()->orderByDesc('title')->get(); livewire(PostsTable::class) ->sortTable('title') - ->assertCanSeeTableRecords($posts->sortBy('title'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('title', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('title'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with relationship', function (): void { - $posts = Post::factory()->count(10)->create(); + Post::factory()->count(10)->create(); + + $sortedAsc = Post::query() + ->orderBy( + User::query() + ->select('name') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + User::query() + ->select('name') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); livewire(PostsTable::class) ->sortTable('author.name') - ->assertCanSeeTableRecords($posts->sortBy('author.name'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.name', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('author.name'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with nested relationship', function (): void { + Post::factory()->count(5)->state(fn (): array => [ + 'author_id' => User::factory()->state([ + 'team_id' => Team::factory(), + ]), + ])->create(); + + $sortedAsc = Post::query() + ->orderBy( + Team::query() + ->select('teams.name') + ->whereColumn('teams.id', 'users.team_id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Team::query() + ->select('teams.name') + ->whereColumn('teams.id', 'users.team_id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('author.team.name') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('author.team.name', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with JSON column', function (): void { @@ -51,11 +116,15 @@ it('can sort records with JSON column', function (): void { 'json' => ['foo' => Str::random()], ])->create(); + // Get database-sorted results to match actual query behavior + $sortedAsc = Post::query()->orderBy('json->foo')->get(); + $sortedDesc = Post::query()->orderByDesc('json->foo')->get(); + livewire(PostsTable::class) ->sortTable('json.foo') - ->assertCanSeeTableRecords($posts->sortBy('json.foo'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('json.foo', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('json.foo'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with nested JSON column', function (): void { @@ -63,11 +132,15 @@ it('can sort records with nested JSON column', function (): void { 'json' => ['bar' => ['baz' => Str::random()]], ])->create(); + // Get database-sorted results to match actual query behavior + $sortedAsc = Post::query()->orderBy('json->bar->baz')->get(); + $sortedDesc = Post::query()->orderByDesc('json->bar->baz')->get(); + livewire(PostsTable::class) ->sortTable('json.bar.baz') - ->assertCanSeeTableRecords($posts->sortBy('json.bar.baz'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('json.bar.baz', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('json.bar.baz'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with relationship JSON column', function (): void { @@ -75,11 +148,30 @@ it('can sort records with relationship JSON column', function (): void { 'author_id' => User::factory()->state(['json' => ['foo' => Str::random()]]), ])->create(); + // Get database-sorted results using subquery orderBy + $sortedAsc = Post::query() + ->orderBy( + User::query() + ->select('json->foo') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + User::query() + ->select('json->foo') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + livewire(PostsTable::class) ->sortTable('author.json.foo') - ->assertCanSeeTableRecords($posts->sortBy('author.json.foo'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.json.foo', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('author.json.foo'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with relationship nested JSON column', function (): void { @@ -87,11 +179,30 @@ it('can sort records with relationship nested JSON column', function (): void { 'author_id' => User::factory()->state(['json' => ['bar' => ['baz' => Str::random()]]]), ])->create(); + // Get database-sorted results using subquery orderBy + $sortedAsc = Post::query() + ->orderBy( + User::query() + ->select('json->bar->baz') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + User::query() + ->select('json->bar->baz') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + livewire(PostsTable::class) ->sortTable('author.json.bar.baz') - ->assertCanSeeTableRecords($posts->sortBy('author.json.bar.baz'), inOrder: true) + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.json.bar.baz', 'desc') - ->assertCanSeeTableRecords($posts->sortByDesc('author.json.bar.baz'), inOrder: true); + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can search records', function (): void { @@ -407,3 +518,930 @@ it('can toggle all table columns', function (): void { ->toggleAllTableColumns(false) ->assertDontSeeText('Toggleable column state'); }); + +it('can search and sort by relationship column when both tables have the same column name', function (): void { + $teamAlpha = Team::factory()->create(['name' => 'Team Alpha']); + $teamBeta = Team::factory()->create(['name' => 'Team Beta']); + + $userAlice = User::factory()->create([ + 'name' => 'Alice', + 'team_id' => $teamAlpha->id, + ]); + + $userBob = User::factory()->create([ + 'name' => 'Bob', + 'team_id' => $teamBeta->id, + ]); + + livewire(UsersWithTeamTable::class) + ->searchTable('Alice') + ->sortTable('team.name') + ->assertCanSeeTableRecords([$userAlice]) + ->assertCanNotSeeTableRecords([$userBob]); +}); + +it('can search and sort by nested relationship column when tables have the same column name, with a qualified column', function (): void { + // Create teams (teams table has 'name' column) + $teamAlpha = Team::factory()->create(['name' => 'Team Alpha']); + $teamBeta = Team::factory()->create(['name' => 'Team Beta']); + $teamGamma = Team::factory()->create(['name' => 'Team Gamma']); + + // Create users with names and team assignments (users table has 'name' column) + $userAlice = User::factory()->create([ + 'name' => 'Alice', + 'team_id' => $teamAlpha->id, + ]); + + $userBob = User::factory()->create([ + 'name' => 'Bob', + 'team_id' => $teamBeta->id, + ]); + + $userCharlie = User::factory()->create([ + 'name' => 'Charlie', + 'team_id' => $teamGamma->id, + ]); + + // Create posts with those authors (posts table has searchable 'title' column) + $postByAlice = Post::factory()->create([ + 'author_id' => $userAlice->id, + 'title' => 'Alice\'s Post', + ]); + + $postByBob = Post::factory()->create([ + 'author_id' => $userBob->id, + 'title' => 'Bob\'s Post', + ]); + + $postByCharlie = Post::factory()->create([ + 'author_id' => $userCharlie->id, + 'title' => 'Charlie\'s Post', + ]); + + livewire(PostsTableWithQualifiedColumns::class) + ->searchTable('Alice') + ->sortTable('author.team.name') + ->assertCanSeeTableRecords([$postByAlice]) + ->assertCanNotSeeTableRecords([$postByBob, $postByCharlie]); +}); + +it('can sort records with `BelongsTo` -> `HasOne` relationship', function (): void { + Post::factory()->count(5)->state(fn (): array => [ + 'author_id' => User::factory()->has( + Profile::factory()->state(fn (): array => [ + 'bio' => fake()->sentence(), + ]), + 'profile' + ), + ])->create(); + + $sortedAsc = Post::query() + ->orderBy( + Profile::query() + ->select('bio') + ->whereColumn('profiles.user_id', 'users.id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Profile::query() + ->select('bio') + ->whereColumn('profiles.user_id', 'users.id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('author.profile.bio') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('author.profile.bio', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void { + Post::factory()->count(5)->state(fn (): array => [ + 'author_id' => User::factory()->has( + Profile::factory()->state(fn (): array => [ + 'company_id' => Company::factory(), + ]), + 'profile' + ), + ])->create(); + + $sortedAsc = Post::query() + ->orderBy( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'profiles.company_id') + ->join('profiles', 'profiles.company_id', '=', 'companies.id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'profiles.company_id') + ->join('profiles', 'profiles.company_id', '=', 'companies.id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('author.profile.company.name') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('author.profile.company.name', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can search records with `BelongsTo` -> `HasOne` relationship', function (): void { + $searchBio = 'unique bio for testing'; + + $matchingUser = User::factory()->has( + Profile::factory()->state([ + 'bio' => $searchBio, + ]), + 'profile' + )->create(); + + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchBio) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); + +it('can search records with `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void { + $searchCompany = 'Acme Corporation Testing'; + + $company = Company::factory()->create([ + 'name' => $searchCompany, + ]); + + $matchingUser = User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchCompany) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); + +it('can sort records with `HasOne` -> `BelongsTo` relationship', function (): void { + for ($i = 0; $i < 5; $i++) { + $company = Company::factory()->create(['name' => 'Company ' . chr(65 + $i)]); // A, B, C, D, E + User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + } + + $sortedAsc = User::query() + ->orderBy( + Company::query() + ->select('name') + ->whereColumn('companies.id', 'profiles.company_id') + ->join('profiles', 'profiles.company_id', '=', 'companies.id') + ->whereColumn('profiles.user_id', 'users.id') + ->limit(1) + ) + ->get(); + + $sortedDesc = User::query() + ->orderByDesc( + Company::query() + ->select('name') + ->whereColumn('companies.id', 'profiles.company_id') + ->join('profiles', 'profiles.company_id', '=', 'companies.id') + ->whereColumn('profiles.user_id', 'users.id') + ->limit(1) + ) + ->get(); + + livewire(UsersTable::class) + ->sortTable('profile.company.name') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('profile.company.name', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with `HasOne` -> `HasOne` relationship', function (): void { + $themes = ['alpha', 'beta', 'gamma', 'delta', 'epsilon']; + + foreach ($themes as $theme) { + User::factory()->has( + Profile::factory()->has( + Setting::factory()->state(['theme' => $theme]), + 'setting' + ), + 'profile' + )->create(); + } + + $sortedAsc = User::query() + ->orderBy( + Setting::query() + ->select('theme') + ->whereColumn('settings.profile_id', 'profiles.id') + ->join('profiles', 'profiles.id', '=', 'settings.profile_id') + ->whereColumn('profiles.user_id', 'users.id') + ->limit(1) + ) + ->get(); + + $sortedDesc = User::query() + ->orderByDesc( + Setting::query() + ->select('theme') + ->whereColumn('settings.profile_id', 'profiles.id') + ->join('profiles', 'profiles.id', '=', 'settings.profile_id') + ->whereColumn('profiles.user_id', 'users.id') + ->limit(1) + ) + ->get(); + + livewire(UsersTable::class) + ->sortTable('profile.setting.theme') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('profile.setting.theme', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can search records with `HasOne` -> `BelongsTo` relationship', function (): void { + $searchCompany = 'TechCorp Testing Inc'; + + $company = Company::factory()->create(['name' => $searchCompany]); + + $matchingUser = User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + + $nonMatchingUsers = User::factory()->count(3)->create(); + + livewire(UsersTable::class) + ->searchTable($searchCompany) + ->assertCanSeeTableRecords([$matchingUser]) + ->assertCanNotSeeTableRecords($nonMatchingUsers); +}); + +it('can search records with `HasOne` -> `HasOne` relationship', function (): void { + $searchTheme = 'midnight-purple'; + + $matchingUser = User::factory()->has( + Profile::factory()->has( + Setting::factory()->state(['theme' => $searchTheme]), + 'setting' + ), + 'profile' + )->create(); + + $nonMatchingUsers = User::factory()->count(3)->create(); + + livewire(UsersTable::class) + ->searchTable($searchTheme) + ->assertCanSeeTableRecords([$matchingUser]) + ->assertCanNotSeeTableRecords($nonMatchingUsers); +}); + +it('can sort records with `MorphOne` relationship', function (): void { + $urls = ['alpha.jpg', 'beta.jpg', 'gamma.jpg', 'delta.jpg', 'epsilon.jpg']; + + foreach ($urls as $url) { + $user = User::factory()->create(); + Image::factory()->create([ + 'url' => $url, + 'imageable_type' => User::class, + 'imageable_id' => $user->id, + ]); + } + + $sortedAsc = User::query() + ->orderBy( + Image::query() + ->select('url') + ->whereColumn('images.imageable_id', 'users.id') + ->where('images.imageable_type', User::class) + ->limit(1) + ) + ->get(); + + $sortedDesc = User::query() + ->orderByDesc( + Image::query() + ->select('url') + ->whereColumn('images.imageable_id', 'users.id') + ->where('images.imageable_type', User::class) + ->limit(1) + ) + ->get(); + + livewire(UsersTable::class) + ->sortTable('image.url') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('image.url', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with `BelongsTo` -> `MorphOne` relationship', function (): void { + $urls = ['alpha.jpg', 'beta.jpg', 'gamma.jpg', 'delta.jpg', 'epsilon.jpg']; + + foreach ($urls as $url) { + $user = User::factory()->create(); + Image::factory()->create([ + 'url' => $url, + 'imageable_type' => User::class, + 'imageable_id' => $user->id, + ]); + Post::factory()->create(['author_id' => $user->id]); + } + + $sortedAsc = Post::query() + ->orderBy( + Image::query() + ->select('url') + ->whereColumn('images.imageable_id', 'users.id') + ->where('images.imageable_type', User::class) + ->join('users', 'users.id', '=', 'images.imageable_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Image::query() + ->select('url') + ->whereColumn('images.imageable_id', 'users.id') + ->where('images.imageable_type', User::class) + ->join('users', 'users.id', '=', 'images.imageable_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('author.image.url') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('author.image.url', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with `BelongsTo` -> `HasOne` -> `MorphOne` relationship', function (): void { + $altTexts = ['alt alpha', 'alt beta', 'alt gamma', 'alt delta', 'alt epsilon']; + + foreach ($altTexts as $altText) { + $user = User::factory()->create(); + $profile = Profile::factory()->create(['user_id' => $user->id]); + Image::factory()->create([ + 'alt_text' => $altText, + 'imageable_type' => Profile::class, + 'imageable_id' => $profile->id, + ]); + Post::factory()->create(['author_id' => $user->id]); + } + + $sortedAsc = Post::query() + ->orderBy( + Image::query() + ->select('alt_text') + ->whereColumn('images.imageable_id', 'profiles.id') + ->where('images.imageable_type', Profile::class) + ->join('profiles', 'profiles.id', '=', 'images.imageable_id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Image::query() + ->select('alt_text') + ->whereColumn('images.imageable_id', 'profiles.id') + ->where('images.imageable_type', Profile::class) + ->join('profiles', 'profiles.id', '=', 'images.imageable_id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('author.profile.image.alt_text') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('author.profile.image.alt_text', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can search records with `MorphOne` relationship', function (): void { + $searchUrl = 'unique-image-url.jpg'; + + $matchingUser = User::factory()->create(); + Image::factory()->create([ + 'url' => $searchUrl, + 'imageable_type' => User::class, + 'imageable_id' => $matchingUser->id, + ]); + + $nonMatchingUsers = User::factory()->count(3)->create(); + + livewire(UsersTable::class) + ->searchTable($searchUrl) + ->assertCanSeeTableRecords([$matchingUser]) + ->assertCanNotSeeTableRecords($nonMatchingUsers); +}); + +it('can search records with `BelongsTo` -> `MorphOne` relationship', function (): void { + $searchUrl = 'unique-author-image.jpg'; + + $matchingUser = User::factory()->create(); + Image::factory()->create([ + 'url' => $searchUrl, + 'imageable_type' => User::class, + 'imageable_id' => $matchingUser->id, + ]); + + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchUrl) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); + +it('can search records with `BelongsTo` -> `HasOne` -> `MorphOne` relationship', function (): void { + $searchAltText = 'unique profile image alt text'; + + $matchingUser = User::factory()->create(); + $matchingProfile = Profile::factory()->create(['user_id' => $matchingUser->id]); + Image::factory()->create([ + 'alt_text' => $searchAltText, + 'imageable_type' => Profile::class, + 'imageable_id' => $matchingProfile->id, + ]); + + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchAltText) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); + +it('can sort records with nullable `BelongsTo` relationship', function (): void { + // Create posts with authors + $userAlice = User::factory()->create(['name' => 'Alice']); + $userBob = User::factory()->create(['name' => 'Bob']); + + $postWithAlice = Post::factory()->create(['author_id' => $userAlice->id]); + $postWithBob = Post::factory()->create(['author_id' => $userBob->id]); + + // Create posts without authors (null relationship) + $postWithoutAuthor1 = Post::factory()->create(['author_id' => null]); + $postWithoutAuthor2 = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postWithAlice, $postWithBob, $postWithoutAuthor1, $postWithoutAuthor2]); + + // Just verify sorting doesn't crash with nullable relationships + livewire(PostsTable::class) + ->sortTable('author.name') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('author.name', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can sort records with nullable `HasOne` relationship', function (): void { + // Create users with profiles + $userWithProfile1 = User::factory()->has( + Profile::factory()->state(['bio' => 'Alpha bio']), + 'profile' + )->create(); + + $userWithProfile2 = User::factory()->has( + Profile::factory()->state(['bio' => 'Beta bio']), + 'profile' + )->create(); + + // Create users without profiles (null relationship) + $userWithoutProfile1 = User::factory()->create(); + $userWithoutProfile2 = User::factory()->create(); + + $allUsers = collect([$userWithProfile1, $userWithProfile2, $userWithoutProfile1, $userWithoutProfile2]); + + // Just verify sorting doesn't crash with nullable relationships + livewire(UsersTable::class) + ->sortTable('profile.bio') + ->assertCanSeeTableRecords($allUsers) + ->sortTable('profile.bio', 'desc') + ->assertCanSeeTableRecords($allUsers); +}); + +it('can sort records with nullable `MorphOne` relationship', function (): void { + // Create users with images + $userWithImage1 = User::factory()->create(); + Image::factory()->create([ + 'url' => 'alpha.jpg', + 'imageable_type' => User::class, + 'imageable_id' => $userWithImage1->id, + ]); + + $userWithImage2 = User::factory()->create(); + Image::factory()->create([ + 'url' => 'beta.jpg', + 'imageable_type' => User::class, + 'imageable_id' => $userWithImage2->id, + ]); + + // Create users without images (null relationship) + $userWithoutImage1 = User::factory()->create(); + $userWithoutImage2 = User::factory()->create(); + + $allUsers = collect([$userWithImage1, $userWithImage2, $userWithoutImage1, $userWithoutImage2]); + + // Just verify sorting doesn't crash with nullable relationships + livewire(UsersTable::class) + ->sortTable('image.url') + ->assertCanSeeTableRecords($allUsers) + ->sortTable('image.url', 'desc') + ->assertCanSeeTableRecords($allUsers); +}); + +it('can sort records with nullable nested `BelongsTo` -> `HasOne` relationship', function (): void { + // Post with author that has profile + $userWithProfile = User::factory()->has( + Profile::factory()->state(['bio' => 'Alpha bio']), + 'profile' + )->create(); + $postWithAuthorAndProfile = Post::factory()->create(['author_id' => $userWithProfile->id]); + + // Post with author but no profile + $userWithoutProfile = User::factory()->create(); + $postWithAuthorNoProfile = Post::factory()->create(['author_id' => $userWithoutProfile->id]); + + // Post with no author + $postWithoutAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postWithAuthorAndProfile, $postWithAuthorNoProfile, $postWithoutAuthor]); + + // Just verify sorting doesn't crash with nullable nested relationships + livewire(PostsTable::class) + ->sortTable('author.profile.bio') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('author.profile.bio', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can sort records with nullable nested `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void { + // Post with author->profile->company + $company = Company::factory()->create(['name' => 'Acme Corp']); + $userWithProfileAndCompany = User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + $postComplete = Post::factory()->create(['author_id' => $userWithProfileAndCompany->id]); + + // Post with author and profile but no company + $userWithProfileNoCompany = User::factory()->has( + Profile::factory()->state(['company_id' => null]), + 'profile' + )->create(); + $postNoCompany = Post::factory()->create(['author_id' => $userWithProfileNoCompany->id]); + + // Post with author but no profile + $userNoProfile = User::factory()->create(); + $postNoProfile = Post::factory()->create(['author_id' => $userNoProfile->id]); + + // Post with no author + $postNoAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postComplete, $postNoCompany, $postNoProfile, $postNoAuthor]); + + // Just verify sorting doesn't crash with nullable nested relationships + livewire(PostsTable::class) + ->sortTable('author.profile.company.name') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('author.profile.company.name', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can sort records with nullable nested `BelongsTo` -> `MorphOne` relationship', function (): void { + // Post with author that has image + $userWithImage = User::factory()->create(); + Image::factory()->create([ + 'url' => 'alpha.jpg', + 'imageable_type' => User::class, + 'imageable_id' => $userWithImage->id, + ]); + $postWithAuthorAndImage = Post::factory()->create(['author_id' => $userWithImage->id]); + + // Post with author but no image + $userWithoutImage = User::factory()->create(); + $postWithAuthorNoImage = Post::factory()->create(['author_id' => $userWithoutImage->id]); + + // Post with no author + $postWithoutAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postWithAuthorAndImage, $postWithAuthorNoImage, $postWithoutAuthor]); + + // Just verify sorting doesn't crash with nullable nested relationships + livewire(PostsTable::class) + ->sortTable('author.image.url') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('author.image.url', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can search records with nullable `BelongsTo` relationship', function (): void { + $userAlice = User::factory()->create(['name' => 'Alice Unique']); + + $matchingPost = Post::factory()->create(['author_id' => $userAlice->id]); + $postWithoutAuthor = Post::factory()->create(['author_id' => null]); + $nonMatchingPost = Post::factory()->create(); + + livewire(PostsTable::class) + ->searchTable('Alice Unique') + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords([$postWithoutAuthor, $nonMatchingPost]); +}); + +it('can search records with nullable `HasOne` relationship', function (): void { + $userWithProfile = User::factory()->has( + Profile::factory()->state(['bio' => 'Unique bio search']), + 'profile' + )->create(); + + $userWithoutProfile = User::factory()->create(); + $otherUsers = User::factory()->count(2)->create(); + + livewire(UsersTable::class) + ->searchTable('Unique bio search') + ->assertCanSeeTableRecords([$userWithProfile]) + ->assertCanNotSeeTableRecords([$userWithoutProfile, ...$otherUsers]); +}); + +it('can sort records with `BelongsToThrough` relationship', function (): void { + Post::factory()->count(5)->state(fn (): array => [ + 'author_id' => User::factory()->state([ + 'team_id' => Team::factory(), + ]), + ])->create(); + + $sortedAsc = Post::query() + ->orderBy( + Team::query() + ->select('teams.name') + ->whereColumn('teams.id', 'users.team_id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Team::query() + ->select('teams.name') + ->whereColumn('teams.id', 'users.team_id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('team.name') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('team.name', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with nullable `BelongsToThrough` relationship', function (): void { + // Post with author that has team + $teamAlpha = Team::factory()->create(['name' => 'Alpha Team']); + $userWithTeam = User::factory()->create(['team_id' => $teamAlpha->id]); + $postWithTeam = Post::factory()->create(['author_id' => $userWithTeam->id]); + + // Post with author but no team + $userWithoutTeam = User::factory()->create(['team_id' => null]); + $postWithAuthorNoTeam = Post::factory()->create(['author_id' => $userWithoutTeam->id]); + + // Post with no author + $postWithoutAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postWithTeam, $postWithAuthorNoTeam, $postWithoutAuthor]); + + // Just verify sorting doesn't crash with nullable relationships + livewire(PostsTable::class) + ->sortTable('team.name') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('team.name', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can search records with `BelongsToThrough` relationship', function (): void { + $searchTeam = 'Unique Team Name'; + + $team = Team::factory()->create(['name' => $searchTeam]); + $matchingUser = User::factory()->create(['team_id' => $team->id]); + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchTeam) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); + +it('can sort records with nested `BelongsToThrough` -> `BelongsTo` relationship', function (): void { + Post::factory()->count(5)->state(fn (): array => [ + 'author_id' => User::factory()->state([ + 'team_id' => Team::factory()->state([ + 'company_id' => Company::factory(), + ]), + ]), + ])->create(); + + $sortedAsc = Post::query() + ->orderBy( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'teams.company_id') + ->join('teams', 'teams.company_id', '=', 'companies.id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'teams.company_id') + ->join('teams', 'teams.company_id', '=', 'companies.id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('team.company.name') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('team.company.name', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with nullable nested `BelongsToThrough` -> `BelongsTo` relationship', function (): void { + // Post with team and company + $company = Company::factory()->create(['name' => 'Acme Corp']); + $teamWithCompany = Team::factory()->create(['company_id' => $company->id]); + $userWithTeamAndCompany = User::factory()->create(['team_id' => $teamWithCompany->id]); + $postComplete = Post::factory()->create(['author_id' => $userWithTeamAndCompany->id]); + + // Post with team but no company + $teamWithoutCompany = Team::factory()->create(['company_id' => null]); + $userWithTeamNoCompany = User::factory()->create(['team_id' => $teamWithoutCompany->id]); + $postNoCompany = Post::factory()->create(['author_id' => $userWithTeamNoCompany->id]); + + // Post with author but no team + $userWithoutTeam = User::factory()->create(['team_id' => null]); + $postNoTeam = Post::factory()->create(['author_id' => $userWithoutTeam->id]); + + // Post with no author + $postNoAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postComplete, $postNoCompany, $postNoTeam, $postNoAuthor]); + + // Just verify sorting doesn't crash with nullable nested relationships + livewire(PostsTable::class) + ->sortTable('team.company.name') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('team.company.name', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can search records with nested `BelongsToThrough` -> `BelongsTo` relationship', function (): void { + $searchCompany = 'Unique Company Name'; + + $company = Company::factory()->create(['name' => $searchCompany]); + $team = Team::factory()->create(['company_id' => $company->id]); + $matchingUser = User::factory()->create(['team_id' => $team->id]); + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchCompany) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); + +it('can sort records with `BelongsTo` -> `BelongsToThrough` relationship', function (): void { + Post::factory()->count(5)->state(fn (): array => [ + 'author_id' => User::factory()->state([ + 'team_id' => Team::factory()->state([ + 'company_id' => Company::factory(), + ]), + ]), + ])->create(); + + $sortedAsc = Post::query() + ->orderBy( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'teams.company_id') + ->join('teams', 'teams.company_id', '=', 'companies.id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + $sortedDesc = Post::query() + ->orderByDesc( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'teams.company_id') + ->join('teams', 'teams.company_id', '=', 'companies.id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->sortTable('author.company.name') + ->assertCanSeeTableRecords($sortedAsc, inOrder: true) + ->sortTable('author.company.name', 'desc') + ->assertCanSeeTableRecords($sortedDesc, inOrder: true); +}); + +it('can sort records with nullable `BelongsTo` -> `BelongsToThrough` relationship', function (): void { + // Post with author, team, and company + $company = Company::factory()->create(['name' => 'Acme Corp']); + $teamWithCompany = Team::factory()->create(['company_id' => $company->id]); + $userComplete = User::factory()->create(['team_id' => $teamWithCompany->id]); + $postComplete = Post::factory()->create(['author_id' => $userComplete->id]); + + // Post with author and team but no company + $teamWithoutCompany = Team::factory()->create(['company_id' => null]); + $userNoCompany = User::factory()->create(['team_id' => $teamWithoutCompany->id]); + $postNoCompany = Post::factory()->create(['author_id' => $userNoCompany->id]); + + // Post with author but no team + $userNoTeam = User::factory()->create(['team_id' => null]); + $postNoTeam = Post::factory()->create(['author_id' => $userNoTeam->id]); + + // Post with no author + $postNoAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postComplete, $postNoCompany, $postNoTeam, $postNoAuthor]); + + // Just verify sorting doesn't crash with nullable relationships + livewire(PostsTable::class) + ->sortTable('author.company.name') + ->assertCanSeeTableRecords($allPosts) + ->sortTable('author.company.name', 'desc') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can search records with `BelongsTo` -> `BelongsToThrough` relationship', function (): void { + $searchCompany = 'Specific Company Name'; + + $company = Company::factory()->create(['name' => $searchCompany]); + $team = Team::factory()->create(['company_id' => $company->id]); + $matchingUser = User::factory()->create(['team_id' => $team->id]); + $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); + + $nonMatchingPosts = Post::factory()->count(3)->create(); + + livewire(PostsTable::class) + ->searchTable($searchCompany) + ->assertCanSeeTableRecords([$matchingPost]) + ->assertCanNotSeeTableRecords($nonMatchingPosts); +}); diff --git a/tests/src/Tables/Commands/MakeLivewireTableCommandTest.php b/tests/src/Tables/Commands/MakeLivewireTableCommandTest.php index 01b9658a03..00908838c7 100644 --- a/tests/src/Tables/Commands/MakeLivewireTableCommandTest.php +++ b/tests/src/Tables/Commands/MakeLivewireTableCommandTest.php @@ -21,12 +21,18 @@ it('can generate a Livewire table component', function (): void { ]); assertFileExists($path = app_path('Livewire/ListBlogPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileExists($viewPath = resource_path('views/livewire/list-blog-posts.blade.php')); - expect(file_get_contents($viewPath)) - ->toMatchSnapshot(); + expect(file_get_contents($viewPath)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a Livewire table component with a model', function (): void { @@ -38,12 +44,18 @@ it('can generate a Livewire table component with a model', function (): void { ]); assertFileExists($path = app_path('Livewire/ListPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileExists($viewPath = resource_path('views/livewire/list-posts.blade.php')); - expect(file_get_contents($viewPath)) - ->toMatchSnapshot(); + expect(file_get_contents($viewPath)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a Livewire table component with generated columns', function (): void { @@ -56,12 +68,18 @@ it('can generate a Livewire table component with generated columns', function () ]); assertFileExists($path = app_path('Livewire/ListPostsWithColumns.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileExists($viewPath = resource_path('views/livewire/list-posts-with-columns.blade.php')); - expect(file_get_contents($viewPath)) - ->toMatchSnapshot(); + expect(file_get_contents($viewPath)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a Livewire table component in a nested directory', function (): void { @@ -73,12 +91,18 @@ it('can generate a Livewire table component in a nested directory', function (): ]); assertFileExists($path = app_path('Livewire/Blog/ListPosts.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileExists($viewPath = resource_path('views/livewire/blog/list-posts.blade.php')); - expect(file_get_contents($viewPath)) - ->toMatchSnapshot(); + expect(file_get_contents($viewPath)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); it('can generate a Livewire table component for a model in a nested directory', function (): void { @@ -90,10 +114,16 @@ it('can generate a Livewire table component for a model in a nested directory', ]); assertFileExists($path = app_path('Livewire/Blog/ListCategories.php')); - expect(file_get_contents($path)) - ->toMatchSnapshot(); + expect(file_get_contents($path)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } assertFileExists($viewPath = resource_path('views/livewire/blog/list-categories.blade.php')); - expect(file_get_contents($viewPath)) - ->toMatchSnapshot(); + expect(file_get_contents($viewPath)); + if (config('database.default') === 'testing') { + expect(file_get_contents($path)) + ->toMatchSnapshot(); + } }); diff --git a/tests/src/Tables/GroupingTest.php b/tests/src/Tables/GroupingTest.php index 9993018ebf..6c712c16fe 100644 --- a/tests/src/Tables/GroupingTest.php +++ b/tests/src/Tables/GroupingTest.php @@ -2,7 +2,14 @@ use Filament\Tables; use Filament\Tests\Fixtures\Livewire\PostsTable; +use Filament\Tests\Fixtures\Livewire\UsersTable; +use Filament\Tests\Fixtures\Models\Company; +use Filament\Tests\Fixtures\Models\Image; use Filament\Tests\Fixtures\Models\Post; +use Filament\Tests\Fixtures\Models\Profile; +use Filament\Tests\Fixtures\Models\Setting; +use Filament\Tests\Fixtures\Models\Team; +use Filament\Tests\Fixtures\Models\User; use Filament\Tests\Tables\TestCase; use Livewire\Features\SupportTesting\Testable; @@ -41,3 +48,329 @@ it('can group a table', function (): void { ->getLabel()->toBe('Dynamic label'); }); }); + +it('can group records by column', function (): void { + // Create posts with different titles to group by + Post::factory()->create(['title' => 'Apple Post']); + Post::factory()->create(['title' => 'Banana Post']); + Post::factory()->create(['title' => 'Apple Post']); + Post::factory()->create(['title' => 'Cherry Post']); + Post::factory()->create(['title' => 'Banana Post']); + + $sortedPosts = Post::query()->orderBy('title')->get(); + + livewire(PostsTable::class) + ->set('tableGrouping', 'title') + ->assertCanSeeTableRecords($sortedPosts, inOrder: true); +}); + +it('can group records by relationship', function (): void { + // Create users with specific names to control order + $userAlice = User::factory()->create(['name' => 'Alice']); + $userBob = User::factory()->create(['name' => 'Bob']); + $userCharlie = User::factory()->create(['name' => 'Charlie']); + + // Create posts with those authors + Post::factory()->create(['author_id' => $userBob->id]); + Post::factory()->create(['author_id' => $userAlice->id]); + Post::factory()->create(['author_id' => $userCharlie->id]); + Post::factory()->create(['author_id' => $userAlice->id]); + Post::factory()->create(['author_id' => $userBob->id]); + + $sortedPosts = Post::query() + ->orderBy( + User::query() + ->select('name') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->set('tableGrouping', 'author.name') + ->assertCanSeeTableRecords($sortedPosts, inOrder: true); +}); + +it('can group records by nested relationship', function (): void { + // Create teams with specific names to control order + $teamAlpha = Team::factory()->create(['name' => 'Alpha Team']); + $teamBeta = Team::factory()->create(['name' => 'Beta Team']); + $teamGamma = Team::factory()->create(['name' => 'Gamma Team']); + + // Create users with teams + $userWithAlpha = User::factory()->create(['team_id' => $teamAlpha->id]); + $userWithBeta = User::factory()->create(['team_id' => $teamBeta->id]); + $userWithGamma = User::factory()->create(['team_id' => $teamGamma->id]); + + // Create posts with those authors + Post::factory()->create(['author_id' => $userWithBeta->id]); + Post::factory()->create(['author_id' => $userWithAlpha->id]); + Post::factory()->create(['author_id' => $userWithGamma->id]); + Post::factory()->create(['author_id' => $userWithAlpha->id]); + Post::factory()->create(['author_id' => $userWithBeta->id]); + + $sortedPosts = Post::query() + ->orderBy( + Team::query() + ->select('teams.name') + ->whereColumn('teams.id', 'users.team_id') + ->join('users', 'users.team_id', '=', 'teams.id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->set('tableGrouping', 'author.team.name') + ->assertCanSeeTableRecords($sortedPosts, inOrder: true); +}); + +it('can group records by `BelongsTo` -> `HasOne` relationship', function (): void { + // Create posts with unique profile bios + $bios = ['Alpha bio', 'Beta bio', 'Gamma bio', 'Delta bio', 'Epsilon bio']; + foreach ($bios as $bio) { + $user = User::factory()->has( + Profile::factory()->state(['bio' => $bio]), + 'profile' + )->create(); + Post::factory()->create(['author_id' => $user->id]); + } + + $sortedPosts = Post::query() + ->orderBy( + Profile::query() + ->select('bio') + ->whereColumn('profiles.user_id', 'users.id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->set('tableGrouping', 'author.profile.bio') + ->assertCanSeeTableRecords($sortedPosts, inOrder: true); +}); + +it('can group records by `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void { + // Create posts with users that have profiles linked to companies + $companyNames = ['Acme Corp', 'Beta Inc', 'Gamma LLC', 'Delta Co', 'Epsilon Ltd']; + foreach ($companyNames as $companyName) { + $company = Company::factory()->create(['name' => $companyName]); + $user = User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + Post::factory()->create(['author_id' => $user->id]); + } + + $sortedPosts = Post::query() + ->orderBy( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'profiles.company_id') + ->join('profiles', 'profiles.company_id', '=', 'companies.id') + ->join('users', 'users.id', '=', 'profiles.user_id') + ->whereColumn('users.id', 'posts.author_id') + ->limit(1) + ) + ->get(); + + livewire(PostsTable::class) + ->set('tableGrouping', 'author.profile.company.name') + ->assertCanSeeTableRecords($sortedPosts, inOrder: true); +}); + +it('can group records by `HasOne` -> `BelongsTo` relationship', function (): void { + // Create users with profiles linked to different companies + $companyNames = ['Alpha Corp', 'Beta Corp', 'Gamma Corp', 'Delta Corp', 'Epsilon Corp']; + foreach ($companyNames as $companyName) { + $company = Company::factory()->create(['name' => $companyName]); + User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + } + + $sortedUsers = User::query() + ->orderBy( + Company::query() + ->select('companies.name') + ->whereColumn('companies.id', 'profiles.company_id') + ->join('profiles', 'profiles.company_id', '=', 'companies.id') + ->whereColumn('profiles.user_id', 'users.id') + ->limit(1) + ) + ->get(); + + livewire(UsersTable::class) + ->set('tableGrouping', 'profile.company.name') + ->assertCanSeeTableRecords($sortedUsers, inOrder: true); +}); + +it('can group records by `HasOne` -> `HasOne` relationship', function (): void { + // Create users with profiles that have settings + $themes = ['alpha-theme', 'beta-theme', 'gamma-theme', 'delta-theme', 'epsilon-theme']; + foreach ($themes as $theme) { + User::factory()->has( + Profile::factory()->has( + Setting::factory()->state(['theme' => $theme]), + 'setting' + ), + 'profile' + )->create(); + } + + $sortedUsers = User::query() + ->orderBy( + Setting::query() + ->select('theme') + ->whereColumn('settings.profile_id', 'profiles.id') + ->join('profiles', 'profiles.id', '=', 'settings.profile_id') + ->whereColumn('profiles.user_id', 'users.id') + ->limit(1) + ) + ->get(); + + livewire(UsersTable::class) + ->set('tableGrouping', 'profile.setting.theme') + ->assertCanSeeTableRecords($sortedUsers, inOrder: true); +}); + +it('can group records by `MorphOne` relationship', function (): void { + $urls = ['alpha.jpg', 'beta.jpg', 'gamma.jpg', 'delta.jpg', 'epsilon.jpg']; + foreach ($urls as $url) { + $user = User::factory()->create(); + Image::factory()->create([ + 'url' => $url, + 'imageable_type' => User::class, + 'imageable_id' => $user->id, + ]); + } + + $sortedUsers = User::query() + ->orderBy( + Image::query() + ->select('url') + ->whereColumn('images.imageable_id', 'users.id') + ->where('images.imageable_type', User::class) + ->limit(1) + ) + ->get(); + + livewire(UsersTable::class) + ->set('tableGrouping', 'image.url') + ->assertCanSeeTableRecords($sortedUsers, inOrder: true); +}); + +it('can group records with nullable `BelongsTo` relationship', function (): void { + $userAlpha = User::factory()->create(['name' => 'Alpha']); + $userBeta = User::factory()->create(['name' => 'Beta']); + + $postWithAlpha = Post::factory()->create(['author_id' => $userAlpha->id]); + $postWithBeta = Post::factory()->create(['author_id' => $userBeta->id]); + $postWithoutAuthor1 = Post::factory()->create(['author_id' => null]); + $postWithoutAuthor2 = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postWithAlpha, $postWithBeta, $postWithoutAuthor1, $postWithoutAuthor2]); + + // Just verify grouping doesn't crash with nullable relationships + livewire(PostsTable::class) + ->set('tableGrouping', 'author.name') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can group records with nullable `HasOne` relationship', function (): void { + $userWithProfile1 = User::factory()->has( + Profile::factory()->state(['bio' => 'Alpha bio']), + 'profile' + )->create(); + + $userWithProfile2 = User::factory()->has( + Profile::factory()->state(['bio' => 'Beta bio']), + 'profile' + )->create(); + + $userWithoutProfile1 = User::factory()->create(); + $userWithoutProfile2 = User::factory()->create(); + + $allUsers = collect([$userWithProfile1, $userWithProfile2, $userWithoutProfile1, $userWithoutProfile2]); + + // Just verify grouping doesn't crash with nullable relationships + livewire(UsersTable::class) + ->set('tableGrouping', 'profile.bio') + ->assertCanSeeTableRecords($allUsers); +}); + +it('can group records with nullable `MorphOne` relationship', function (): void { + $userWithImage1 = User::factory()->create(); + Image::factory()->create([ + 'url' => 'alpha.jpg', + 'imageable_type' => User::class, + 'imageable_id' => $userWithImage1->id, + ]); + + $userWithImage2 = User::factory()->create(); + Image::factory()->create([ + 'url' => 'beta.jpg', + 'imageable_type' => User::class, + 'imageable_id' => $userWithImage2->id, + ]); + + $userWithoutImage1 = User::factory()->create(); + $userWithoutImage2 = User::factory()->create(); + + $allUsers = collect([$userWithImage1, $userWithImage2, $userWithoutImage1, $userWithoutImage2]); + + // Just verify grouping doesn't crash with nullable relationships + livewire(UsersTable::class) + ->set('tableGrouping', 'image.url') + ->assertCanSeeTableRecords($allUsers); +}); + +it('can group records with nullable nested `BelongsTo` -> `HasOne` relationship', function (): void { + $userWithProfile = User::factory()->has( + Profile::factory()->state(['bio' => 'Alpha bio']), + 'profile' + )->create(); + $postWithAuthorAndProfile = Post::factory()->create(['author_id' => $userWithProfile->id]); + + $userWithoutProfile = User::factory()->create(); + $postWithAuthorNoProfile = Post::factory()->create(['author_id' => $userWithoutProfile->id]); + + $postWithoutAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postWithAuthorAndProfile, $postWithAuthorNoProfile, $postWithoutAuthor]); + + // Just verify grouping doesn't crash with nullable nested relationships + livewire(PostsTable::class) + ->set('tableGrouping', 'author.profile.bio') + ->assertCanSeeTableRecords($allPosts); +}); + +it('can group records with nullable nested `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void { + $company = Company::factory()->create(['name' => 'Acme Corp']); + $userWithProfileAndCompany = User::factory()->has( + Profile::factory()->for($company, 'company'), + 'profile' + )->create(); + $postComplete = Post::factory()->create(['author_id' => $userWithProfileAndCompany->id]); + + $userWithProfileNoCompany = User::factory()->has( + Profile::factory()->state(['company_id' => null]), + 'profile' + )->create(); + $postNoCompany = Post::factory()->create(['author_id' => $userWithProfileNoCompany->id]); + + $userNoProfile = User::factory()->create(); + $postNoProfile = Post::factory()->create(['author_id' => $userNoProfile->id]); + + $postNoAuthor = Post::factory()->create(['author_id' => null]); + + $allPosts = collect([$postComplete, $postNoCompany, $postNoProfile, $postNoAuthor]); + + // Just verify grouping doesn't crash with nullable nested relationships + livewire(PostsTable::class) + ->set('tableGrouping', 'author.profile.company.name') + ->assertCanSeeTableRecords($allPosts); +}); diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php index 2f1cd78261..5c90f803d0 100644 --- a/tests/src/TestCase.php +++ b/tests/src/TestCase.php @@ -30,17 +30,18 @@ use Filament\Tests\Fixtures\Providers\RequiredMultiFactorAuthenticationPanelProv use Filament\Tests\Fixtures\Providers\SlugsPanelProvider; use Filament\Tests\Fixtures\Providers\TenancyPanelProvider; use Filament\Widgets\WidgetsServiceProvider; -use Illuminate\Foundation\Testing\LazilyRefreshDatabase; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Gate; use Kirschbaum\PowerJoins\PowerJoinsServiceProvider; use Livewire\LivewireServiceProvider; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as BaseTestCase; +use PDO; use RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider; abstract class TestCase extends BaseTestCase { - use LazilyRefreshDatabase; + use RefreshDatabase; use WithWorkbench; protected function getPackageProviders($app): array @@ -89,5 +90,51 @@ abstract class TestCase extends BaseTestCase ...$app['config']->get('view.paths'), __DIR__ . '/../resources/views', ]); + + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => env('DB_DATABASE', ':memory:'), + 'prefix' => '', + 'foreign_key_constraints' => true, + ]); + + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('MYSQL_PORT', env('DB_PORT', '3306')), + 'database' => env('DB_DATABASE', 'testing'), + 'username' => env('MYSQL_USERNAME', env('DB_USERNAME', 'root')), + 'password' => env('MYSQL_PASSWORD', env('DB_PASSWORD', '')), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + 'timezone' => '+00:00', + 'options' => [ + PDO::ATTR_TIMEOUT => 5, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ], + ]); + + $app['config']->set('database.connections.pgsql', [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('PGSQL_PORT', env('DB_PORT', '5432')), + 'database' => env('DB_DATABASE', 'testing'), + 'username' => env('PGSQL_USERNAME', env('DB_USERNAME', 'postgres')), + 'password' => env('PGSQL_PASSWORD', env('DB_PASSWORD', '')), + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + 'options' => [ + PDO::ATTR_TIMEOUT => 5, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ], + ]); + + $app['config']->set('database.default', env('DB_CONNECTION', 'testing')); + $app['config']->set('database.connections.testing', $app['config']->get('database.connections.sqlite')); } }