count(10)->create(); livewire(PostsTable::class) ->assertCanRenderTableColumn('title'); }); it('can render text column with relationship', function (): void { Post::factory()->count(10)->create(); livewire(PostsTable::class) ->assertCanRenderTableColumn('author.name'); }); it('can sort records', function (): void { Post::factory()->count(10)->create(); $sortedAsc = Post::query()->orderBy('title')->orderBy('id')->get(); $sortedDesc = Post::query()->orderByDesc('title')->orderBy('id')->get(); livewire(PostsTable::class) ->sortTable('title') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('title', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with relationship', function (): void { Post::factory()->count(10)->create(); $sortedAsc = Post::query() ->orderBy( User::query() ->select('name') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); $sortedDesc = Post::query() ->orderByDesc( User::query() ->select('name') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); livewire(PostsTable::class) ->sortTable('author.name') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.name', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with nested relationship', function (): void { foreach (['Birch', 'Maple', 'Birch', 'Cedar', 'Ash'] as $teamName) { Post::factory()->state([ 'author_id' => User::factory()->state([ 'team_id' => Team::factory()->state(['name' => $teamName]), ]), ])->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) ) ->orderBy('posts.id') ->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) ) ->orderByDesc('posts.id') ->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 by multiple columns in the order they are passed', function (): void { $posts = collect([ ['rating' => 1, 'title' => 'Charlie'], ['rating' => 2, 'title' => 'Alpha'], ['rating' => 1, 'title' => 'Bravo'], ['rating' => 2, 'title' => 'Delta'], ])->map(fn (array $attributes): Post => Post::factory()->create($attributes)); livewire(PostsTable::class) ->sortTable('rating_and_title') ->assertCanSeeTableRecords([$posts[2], $posts[0], $posts[1], $posts[3]], inOrder: true) ->sortTable('rating_and_title', 'desc') ->assertCanSeeTableRecords([$posts[3], $posts[1], $posts[0], $posts[2]], inOrder: true); }); it('can sort records with JSON column', function (): void { $posts = Post::factory()->count(10)->state(fn (): array => [ 'json' => ['foo' => Str::random()], ])->create(); // Get database-sorted results to match actual query behavior $sortedAsc = Post::query()->orderBy('json->foo')->orderBy('id')->get(); $sortedDesc = Post::query()->orderByDesc('json->foo')->orderBy('id')->get(); livewire(PostsTable::class) ->sortTable('json.foo') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('json.foo', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with nested JSON column', function (): void { $posts = Post::factory()->count(10)->state(fn (): array => [ 'json' => ['bar' => ['baz' => Str::random()]], ])->create(); // Get database-sorted results to match actual query behavior $sortedAsc = Post::query()->orderBy('json->bar->baz')->orderBy('id')->get(); $sortedDesc = Post::query()->orderByDesc('json->bar->baz')->orderBy('id')->get(); livewire(PostsTable::class) ->sortTable('json.bar.baz') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('json.bar.baz', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with relationship JSON column', function (): void { $posts = Post::factory()->count(10)->state(fn (): array => [ '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) ) ->orderBy('posts.id') ->get(); $sortedDesc = Post::query() ->orderByDesc( User::query() ->select('json->foo') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); livewire(PostsTable::class) ->sortTable('author.json.foo') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.json.foo', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with relationship nested JSON column', function (): void { $posts = Post::factory()->count(10)->state(fn (): array => [ '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) ) ->orderBy('posts.id') ->get(); $sortedDesc = Post::query() ->orderByDesc( User::query() ->select('json->bar->baz') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); livewire(PostsTable::class) ->sortTable('author.json.bar.baz') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.json.bar.baz', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); }); describe('searching', function (): void { it('can search records', function (): void { $posts = Post::factory()->count(10)->create(); $title = $posts->first()->title; livewire(PostsTable::class) ->searchTable($title) ->assertCanSeeTableRecords($posts->where('title', $title)) ->assertCanNotSeeTableRecords($posts->where('title', '!=', $title)); }); it('treats LIKE wildcard characters as literal text when searching records', function (): void { $literalMatch = Post::factory()->create(['title' => 'Before café!_100% After']); $wildcardMatch = Post::factory()->create(['title' => 'Before café!X100 anything After']); livewire(PostsTable::class) ->searchTable('café!_100%') ->assertCanSeeTableRecords([$literalMatch]) ->assertCanNotSeeTableRecords([$wildcardMatch]); }); it('can search individual column records', function (): void { $posts = Post::factory()->count(10)->create(); $content = $posts->first()->content; livewire(PostsTable::class) ->searchTableColumns(['content' => $content]) ->assertCanSeeTableRecords($posts->where('content', $content)) ->assertCanNotSeeTableRecords($posts->where('content', '!=', $content)); }); it('can search posts with relationship', function (): void { $posts = Post::factory()->count(10)->create(); $author = $posts->first()->author->name; livewire(PostsTable::class) ->searchTable($author) ->assertCanSeeTableRecords($posts->where('author.name', $author)) ->assertCanNotSeeTableRecords($posts->where('author.name', '!=', $author)); }); it('can search posts with JSON column', function (): void { $search = Str::random(); $matchingPosts = Post::factory()->count(5)->create([ 'json' => ['foo' => $search], ]); $notMatchingPosts = Post::factory()->count(5)->create([ 'json' => ['foo' => Str::random()], ]); livewire(PostsTable::class) ->searchTable($search) ->assertCanSeeTableRecords($matchingPosts) ->assertCanNotSeeTableRecords($notMatchingPosts); }); it('can search posts with nested JSON column', function (): void { $search = Str::random(); $matchingPosts = Post::factory()->count(5)->create([ 'json' => ['bar' => ['baz' => $search]], ]); $notMatchingPosts = Post::factory()->count(5)->create([ 'json' => ['bar' => ['baz' => Str::random()]], ]); livewire(PostsTable::class) ->searchTable($search) ->assertCanSeeTableRecords($matchingPosts) ->assertCanNotSeeTableRecords($notMatchingPosts); }); it('can search posts with relationship JSON column', function (): void { $search = Str::random(); $matchingAuthor = User::factory() ->create(['json' => ['foo' => $search]]); $matchingPosts = Post::factory() ->for($matchingAuthor, 'author') ->count(5) ->create(); $notMatchingPosts = Post::factory() ->count(5) ->create(); livewire(PostsTable::class) ->searchTable($search) ->assertCanSeeTableRecords($matchingPosts) ->assertCanNotSeeTableRecords($notMatchingPosts); }); it('can search posts with relationship nested JSON column', function (): void { $search = Str::random(); $matchingAuthor = User::factory() ->create(['json' => ['bar' => ['baz' => $search]]]); $matchingPosts = Post::factory() ->for($matchingAuthor, 'author') ->count(5) ->create(); $notMatchingPosts = Post::factory() ->count(5) ->create(); livewire(PostsTable::class) ->searchTable($search) ->assertCanSeeTableRecords($matchingPosts) ->assertCanNotSeeTableRecords($notMatchingPosts); }); it('can search individual column records with relationship', function (): void { $posts = Post::factory()->count(10)->create(); $authorEmail = $posts->first()->author->email; livewire(PostsTable::class) ->searchTableColumns(['author.email' => $authorEmail]) ->assertCanSeeTableRecords($posts->where('author.email', $authorEmail)) ->assertCanNotSeeTableRecords($posts->where('author.email', '!=', $authorEmail)); }); it('can search multiple individual columns', function (): void { $posts = Post::factory()->count(10)->create(); $content = $posts->first()->content; $authorEmail = $posts->first()->author->email; livewire(PostsTable::class) ->searchTableColumns([ 'content' => $content, 'author.email' => $authorEmail, ]) ->assertCanSeeTableRecords($posts->where('author.email', $authorEmail)) ->assertCanNotSeeTableRecords($posts->where('author.email', '!=', $authorEmail)); }); it('can use `splitIndividualSearchTerms(false)` on a column to disable search term splitting for its individual search', function (): void { $exactPhrasePost = Post::factory()->create(['content' => 'ipsum dolor']); $separateWordsPost = Post::factory()->create(['content' => 'ipsum sit dolor']); livewire(PostsTableWithColumnIndividualSearchTermSplittingDisabled::class) ->searchTableColumns(['content' => 'ipsum dolor']) ->assertCanSeeTableRecords([$exactPhrasePost]) ->assertCanNotSeeTableRecords([$separateWordsPost]); }); it('splits search terms for an individual column search by default, when the column does not define `splitIndividualSearchTerms()`', function (): void { $exactPhrasePost = Post::factory()->create(['title' => 'ipsum dolor']); $separateWordsPost = Post::factory()->create(['title' => 'ipsum sit dolor']); livewire(PostsTableWithColumnIndividualSearchTermSplittingDisabled::class) ->searchTableColumns(['title' => 'ipsum dolor']) ->assertCanSeeTableRecords([$exactPhrasePost, $separateWordsPost]); }); it('can use `splitIndividualSearchTerms()` on a column to enable search term splitting for its individual search when the table uses `splitSearchTerms(false)`', function (): void { $exactPhrasePost = Post::factory()->create(['content' => 'ipsum dolor']); $separateWordsPost = Post::factory()->create(['content' => 'ipsum sit dolor']); livewire(PostsTableWithColumnIndividualSearchTermSplittingEnabled::class) ->searchTableColumns(['content' => 'ipsum dolor']) ->assertCanSeeTableRecords([$exactPhrasePost, $separateWordsPost]); }); it('does not split search terms for an individual column search when the table uses `splitSearchTerms(false)` and the column does not define `splitIndividualSearchTerms()`', function (): void { $exactPhrasePost = Post::factory()->create(['title' => 'ipsum dolor']); $separateWordsPost = Post::factory()->create(['title' => 'ipsum sit dolor']); livewire(PostsTableWithColumnIndividualSearchTermSplittingEnabled::class) ->searchTableColumns(['title' => 'ipsum dolor']) ->assertCanSeeTableRecords([$exactPhrasePost]) ->assertCanNotSeeTableRecords([$separateWordsPost]); }); it('does not leak the `orWhere` of a non-split individual column search across multiple columns past an active filter', function (): void { // The searchable column matches on both `title` and `content`. With `splitSearchTerms(false)`, // the constraint emits `where(title LIKE ?)->orWhere(content LIKE ?)`. If that pair is not // wrapped in its own `where()` group, the trailing `orWhere` escapes the filter group and // re-exposes records the filter excluded. $search = Str::random(); $publishedMatch = Post::factory()->create([ 'is_published' => true, 'content' => $search, ]); $unpublishedMatch = Post::factory()->create([ 'is_published' => false, 'content' => $search, ]); livewire(PostsTableWithNonSplitMultiColumnIndividualSearch::class) ->filterTable('is_published') ->searchTableColumns(['title' => $search]) ->assertCanSeeTableRecords([$publishedMatch]) ->assertCanNotSeeTableRecords([$unpublishedMatch]); }); it('seeds individually searchable columns whose names collide with JS array properties on mount so `tableColumnSearches` serializes as a JSON object', function (): void { livewire(PostsTableWithReservedJsPropertyColumnSearch::class) ->assertSet('tableColumnSearches', ['length' => '', 'sort' => '']); }); it('does not seed individually searchable columns whose names do not collide with JS array properties', function (): void { livewire(PostsTableWithReservedJsPropertyColumnSearch::class) ->assertSet('tableColumnSearches', fn (array $tableColumnSearches): bool => ! array_key_exists('title', $tableColumnSearches)); }); it('does not remove a reserved key from `tableColumnSearches` when clearing an individual column search value', function (): void { livewire(PostsTableWithReservedJsPropertyColumnSearch::class) ->set('tableColumnSearches.length', 'foo') ->assertSet('tableColumnSearches.length', 'foo') ->set('tableColumnSearches.length', '') ->assertSet('tableColumnSearches.length', ''); }); it('removes a non-reserved key from `tableColumnSearches` when clearing an individual column search value', function (): void { livewire(PostsTableWithReservedJsPropertyColumnSearch::class) ->set('tableColumnSearches.title', 'foo') ->assertSet('tableColumnSearches.title', 'foo') ->set('tableColumnSearches.title', '') ->assertSet('tableColumnSearches', fn (array $tableColumnSearches): bool => ! array_key_exists('title', $tableColumnSearches)); }); it('keeps reserved keys present in `tableColumnSearches` after resetting all column searches', function (): void { livewire(PostsTableWithReservedJsPropertyColumnSearch::class) ->set('tableColumnSearches.length', 'foo') ->set('tableColumnSearches.sort', 'bar') ->call('resetTableColumnSearches') ->assertSet('tableColumnSearches', ['length' => '', 'sort' => '']); }); it('renders, responsively hides, and clears individual column search inputs with associated labels for columns named after JavaScript array properties in the browser', function (): void { retry(10, function (): void { Artisan::call('filament:assets'); $this->actingAs(User::factory()->create()); Post::factory()->count(3)->create(); visit('/individual-column-search-browser-test') ->assertValue('.fi-ta-individual-search-cell-length input', '') ->assertValue('.fi-ta-individual-search-cell-sort input', '') ->assertValue('.fi-ta-individual-search-cell-title input', '') ->resize(375, 812) ->assertScript("(() => { const expectedLabels = { length: 'Length', sort: 'Sort', title: 'Title' }; return Object.entries(expectedLabels).every(([column, label]) => document.querySelector('.fi-ta-individual-search-cell-' + column + ' input').labels[0]?.textContent.trim() === label) })()", true) ->fill('.fi-ta-individual-search-cell-length input', 'foo') ->wait(1) ->assertValue('.fi-ta-individual-search-cell-length input', 'foo') ->fill('.fi-ta-individual-search-cell-length input', '') ->wait(1) ->assertValue('.fi-ta-individual-search-cell-length input', '') ->resize(700, 812) ->assertScript("document.querySelector('.fi-ta-individual-search-row').checkVisibility()", false) ->resize(800, 812) ->assertScript("document.querySelector('.fi-ta-individual-search-row').checkVisibility()", true) ->assertNoSmoke() ->assertNoAccessibilityIssues(); visit('/individual-column-search-browser-test') ->inDarkMode() ->resize(375, 812) ->assertNoAccessibilityIssues(); }); }); it('scopes column manager checkbox ids to each table in the browser', function (): void { retry(10, function (): void { Artisan::call('filament:assets'); $this->actingAs(User::factory()->create()); Post::factory()->count(3)->create(); visit('/column-manager-browser-test') ->assertPresent('#first-table .fi-ta-header-cell-title') ->assertPresent('#second-table .fi-ta-header-cell-title') // Open the first table's column manager too, so its checkboxes are also rendered when the second manager's labels are clicked. ->click('#first-table button[aria-label="Column manager"]') ->click('#second-table button[aria-label="Column manager"]') // Every rendered column manager checkbox id must be unique, otherwise a label's `for` can activate a checkbox in another table's manager. ->assertScript('new Set(Array.from(document.querySelectorAll(\'.fi-ta-col-manager-label input[type="checkbox"]\')).map((checkbox) => checkbox.id)).size === document.querySelectorAll(\'.fi-ta-col-manager-label input[type="checkbox"]\').length', true) // Clicking the `Title` label must toggle the checkbox in this table's column manager, not the checkbox of the other table's manager that shares the column name. ->click('#second-table .fi-ta-col-manager-label[for$="-title"]') ->wait(1) ->assertMissing('#second-table .fi-ta-header-cell-title') ->assertPresent('#first-table .fi-ta-header-cell-title') // Restore the toggled column, since the column manager persists in the session and a retried attempt must start from the default state. ->click('#second-table .fi-ta-col-manager-label[for$="-title"]') ->wait(1) ->assertPresent('#second-table .fi-ta-header-cell-title') ->assertNoSmoke() ->assertNoAccessibilityIssues(); visit('/column-manager-browser-test') ->inDarkMode() ->click('#second-table button[aria-label="Column manager"]') ->assertNoAccessibilityIssues(); }); }); }); describe('column properties and assertions', function (): void { it('can hide a column', function (): void { livewire(PostsTable::class) ->assertTableColumnVisible('visible') ->assertTableColumnHidden('hidden'); }); it('can call a column action', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->callTableColumnAction('title', $post) ->assertDispatched('title-action-called'); }); it('can call a column action object', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->callTableAction('column-action-object', $post) ->assertDispatched('column-action-object-called'); }); it('can state whether a column has the correct value with Eloquent records', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->assertTableColumnStateSet('with_state', 'correct state', $post) ->assertTableColumnStateNotSet('with_state', 'incorrect state', $post); }); it('can state whether a column has the correct value with custom data', function (): void { livewire(CustomDataTable::class) ->assertTableColumnStateSet('title', 'Second item', 2) ->assertTableColumnStateNotSet('title', 'incorrect state', 2); }); it('can state whether a column with `counts()` has the correct value', function (): void { $user = User::factory() ->has(Post::factory()->count(3)) ->create(); livewire(UsersTable::class) ->assertTableColumnStateSet('posts_count', 3, $user) ->assertTableColumnStateNotSet('posts_count', 0, $user); }); it('can state whether a column has the correct formatted value with Eloquent records', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->assertTableColumnFormattedStateSet('formatted_state', 'formatted state', $post) ->assertTableColumnFormattedStateNotSet('formatted_state', 'incorrect formatted state', $post); }); it('can state whether a column has the correct formatted value with custom data', function (): void { livewire(CustomDataTable::class) ->assertTableColumnFormattedStateSet('formatted_state', 'formatted state', 1) ->assertTableColumnFormattedStateNotSet('formatted_state', 'incorrect formatted state', 1); }); it('can output JSON values', function (): void { $post = Post::factory()->create([ 'json' => ['foo' => 'bar'], ]); livewire(PostsTable::class) ->assertTableColumnStateSet('json.foo', 'bar', $post); }); it('can output nested JSON values', function (): void { $post = Post::factory()->create([ 'json' => ['bar' => ['baz' => 'qux']], ]); livewire(PostsTable::class) ->assertTableColumnStateSet('json.bar.baz', 'qux', $post); }); it('can output relationship JSON values', function (): void { $post = Post::factory()->create([ 'author_id' => User::factory()->state([ 'json' => ['foo' => 'bar'], ]), ]); livewire(PostsTable::class) ->assertTableColumnStateSet('author.json.foo', 'bar', $post); }); it('can output relationship nested JSON values', function (): void { $post = Post::factory()->create([ 'author_id' => User::factory()->state([ 'json' => ['bar' => ['baz' => 'qux']], ]), ]); livewire(PostsTable::class) ->assertTableColumnStateSet('author.json.bar.baz', 'qux', $post); }); it('can output values in a JSON array column of objects', function (): void { $post = Post::factory()->create([ 'json_array_of_objects' => [ ['value' => 'foo'], ['value' => 'bar'], ['value' => 'baz'], ], ]); livewire(PostsTable::class) ->assertTableColumnStateSet('json_array_of_objects.*.value', ['foo', 'bar', 'baz'], $post); }); it('can output values in a JSON column with a non-relationship accessor method', function (): void { $post = Post::factory()->create([ 'config' => ['setting' => 'foo'], ]); livewire(PostsTable::class) ->assertTableColumnStateSet('config.setting', 'foo', $post); }); it('can state whether a column has extra attributes with Eloquent records', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->assertTableColumnHasExtraAttributes('extra_attributes', ['class' => 'text-danger-500'], $post) ->assertTableColumnDoesNotHaveExtraAttributes('extra_attributes', ['class' => 'text-primary-500'], $post); }); it('can state whether a column has extra attributes with custom data', function (): void { livewire(CustomDataTable::class) ->assertTableColumnHasExtraAttributes('extra_attributes', ['class' => 'text-danger-500'], 1) ->assertTableColumnDoesNotHaveExtraAttributes('extra_attributes', ['class' => 'text-primary-500'], 1); }); it('can state whether a column has a description with Eloquent records', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->assertTableColumnHasDescription('with_description', 'description below', $post) ->assertTableColumnHasDescription('with_description', 'description above', $post, 'above') ->assertTableColumnDoesNotHaveDescription('with_description', 'incorrect description below', $post) ->assertTableColumnDoesNotHaveDescription('with_description', 'incorrect description above', $post, 'above'); }); it('can state whether a column has a description with custom data', function (): void { livewire(CustomDataTable::class) ->assertTableColumnHasDescription('with_description', 'description below', 1) ->assertTableColumnHasDescription('with_description', 'description above', 1, 'above') ->assertTableColumnDoesNotHaveDescription('with_description', 'incorrect description below', 1) ->assertTableColumnDoesNotHaveDescription('with_description', 'incorrect description above', 1, 'above'); }); it('can state whether a select column has options with Eloquent records', function (): void { $post = Post::factory()->create(); livewire(PostsTable::class) ->assertTableSelectColumnHasOptions('with_options', ['red' => 'Red', 'blue' => 'Blue'], $post) ->assertTableSelectColumnDoesNotHaveOptions('with_options', ['one' => 'One', 'two' => 'Two'], $post); }); it('can state whether a select column has options with custom data', function (): void { livewire(CustomDataTable::class) ->assertTableSelectColumnHasOptions('with_options', ['red' => 'Red', 'blue' => 'Blue'], 2) ->assertTableSelectColumnDoesNotHaveOptions('with_options', ['one' => 'One', 'two' => 'Two'], 2); }); it('can assert that a column exists with the given configuration', function (): void { $publishedPost = Post::factory()->create([ 'is_published' => true, ]); livewire(PostsTable::class) ->assertTableColumnExists('title2', function (TextColumn $column) { return $column->isSortable() && $column->isSearchable() && $column->getPrefix() == 'published'; }, $publishedPost); $unpublishedPost = Post::factory()->create([ 'is_published' => false, ]); livewire(PostsTable::class) ->assertTableColumnExists('title2', function (TextColumn $column) { return $column->getPrefix() == 'unpublished'; }, $unpublishedPost); $this->expectException('PHPUnit\Framework\ExpectationFailedException'); $this->expectExceptionMessage('Failed asserting that a column with the name [title] and provided configuration exists on the [' . PostsTable::class . '] component'); livewire(PostsTable::class) ->assertTableColumnExists('title', function (TextColumn $column) { return $column->isTime(); }, $publishedPost); }); it('can automatically detect boolean cast attribute in icon column', function (): void { $post = Post::factory() ->create(['is_published' => false]); livewire(PostsTable::class) ->assertTableColumnExists('is_published', function (IconColumn $column) { return $column->isBoolean(); }, $post); }); it('can toggle all table columns', function (): void { Post::factory()->create(); livewire(PostsTable::class) ->assertSuccessful() ->assertCountTableRecords(1) ->assertDontSeeText('Toggleable column state') ->toggleAllTableColumns() ->assertSeeText('Toggleable column state') ->toggleAllTableColumns(false) ->assertDontSeeText('Toggleable column state'); }); }); describe('relationship columns', function (): void { it('can output the state of a nested relationship through a `MorphTo` relationship', function (): void { $team = Team::factory()->create(['name' => 'Team Alpha']); $user = User::factory()->create([ 'name' => 'Alice', 'team_id' => $team->id, ]); $image = Image::factory()->for($user, 'imageable')->create(); livewire(ImagesTable::class) ->assertTableColumnStateSet('imageable.team.name', 'Team Alpha', $image) ->assertTableColumnStateNotSet('imageable.team.name', 'Alice', $image) ->assertTableColumnFormattedStateSet('imageable.team.name', 'Team Alpha', $image) ->assertTableColumnFormattedStateNotSet('imageable.team.name', 'Alice', $image); }); it('can output the state of multiple nested relationships through the same `MorphTo` relationship', function (): void { $company = Company::factory()->create(['name' => 'Acme Corporation']); $team = Team::factory()->create([ 'name' => 'Team Alpha', 'company_id' => $company->id, ]); $user = User::factory()->create([ 'name' => 'Alice', 'team_id' => $team->id, ]); $image = Image::factory()->for($user, 'imageable')->create(); livewire(ImagesTable::class) ->assertTableColumnStateSet('imageable.team.name', 'Team Alpha', $image) ->assertTableColumnStateSet('imageable.company.name', 'Acme Corporation', $image); }); it('can output the state of nested relationships through a `MorphTo` relationship with mixed related model types', function (): void { $userCompany = Company::factory()->create(['name' => 'Acme Corporation']); $team = Team::factory()->create(['company_id' => $userCompany->id]); $user = User::factory()->create([ 'name' => 'Alice', 'team_id' => $team->id, ]); $userImage = Image::factory()->for($user, 'imageable')->create(); $profileCompany = Company::factory()->create(['name' => 'Globex Corporation']); $profile = Profile::factory()->create([ 'user_id' => $user->id, 'company_id' => $profileCompany->id, ]); $profileImage = Image::factory()->for($profile, 'imageable')->create(); livewire(ImagesTable::class) ->assertTableColumnStateSet('imageable.company.name', 'Acme Corporation', $userImage) ->assertTableColumnStateSet('imageable.company.name', 'Globex Corporation', $profileImage); }); 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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('users.id') ->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) ) ->orderBy('users.id') ->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) ) ->orderBy('users.id') ->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) ) ->orderBy('users.id') ->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) ) ->orderBy('users.id') ->get(); $sortedDesc = User::query() ->orderByDesc( Image::query() ->select('url') ->whereColumn('images.imageable_id', 'users.id') ->where('images.imageable_type', User::class) ->limit(1) ) ->orderBy('users.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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) ) ->orderBy('posts.id') ->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); }); it('can sort records with `HasOneThrough` 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') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); $sortedDesc = User::query() ->orderByDesc( Setting::query() ->select('theme') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); livewire(UsersTable::class) ->sortTable('setting.theme') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('setting.theme', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with nullable `HasOneThrough` relationship', function (): void { // User with profile and setting $userWithSetting = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['theme' => 'dark']), 'setting' ), 'profile' )->create(); // User with profile but no setting $userWithProfileNoSetting = User::factory()->has( Profile::factory(), 'profile' )->create(); // User without profile (no setting possible) $userWithoutProfile = User::factory()->create(); $allUsers = collect([$userWithSetting, $userWithProfileNoSetting, $userWithoutProfile]); // Just verify sorting doesn't crash with nullable relationships livewire(UsersTable::class) ->sortTable('setting.theme') ->assertCanSeeTableRecords($allUsers) ->sortTable('setting.theme', 'desc') ->assertCanSeeTableRecords($allUsers); }); it('can search records with `HasOneThrough` relationship', function (): void { $searchTheme = 'unique-theme-for-testing'; $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 `HasOneThrough` relationship using different column', function (): void { $languages = ['de', 'en', 'es', 'fr', 'it']; foreach ($languages as $language) { User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language' => $language]), 'setting' ), 'profile' )->create(); } $sortedAsc = User::query() ->orderBy( Setting::query() ->select('language') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); $sortedDesc = User::query() ->orderByDesc( Setting::query() ->select('language') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); livewire(UsersTable::class) ->sortTable('setting.language') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('setting.language', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can search records with `HasOneThrough` relationship using different column', function (): void { $searchLanguage = 'unique-language'; $matchingUser = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language' => $searchLanguage]), 'setting' ), 'profile' )->create(); $nonMatchingUsers = User::factory()->count(3)->create(); livewire(UsersTable::class) ->searchTable($searchLanguage) ->assertCanSeeTableRecords([$matchingUser]) ->assertCanNotSeeTableRecords($nonMatchingUsers); }); it('can sort records with `BelongsTo` -> `HasOneThrough` relationship', function (): void { $themes = ['alpha', 'beta', 'gamma', 'delta', 'epsilon']; foreach ($themes as $theme) { $user = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['theme' => $theme]), 'setting' ), 'profile' )->create(); Post::factory()->create(['author_id' => $user->id]); } $sortedAsc = Post::query() ->orderBy( Setting::query() ->select('theme') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->join('users', 'users.id', '=', 'profiles.user_id') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); $sortedDesc = Post::query() ->orderByDesc( Setting::query() ->select('theme') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->join('users', 'users.id', '=', 'profiles.user_id') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); livewire(PostsTable::class) ->sortTable('author.setting.theme') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.setting.theme', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with nullable `BelongsTo` -> `HasOneThrough` relationship', function (): void { // Post with author that has setting $userWithSetting = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['theme' => 'dark']), 'setting' ), 'profile' )->create(); $postWithSetting = Post::factory()->create(['author_id' => $userWithSetting->id]); // Post with author with profile but no setting $userWithProfileNoSetting = User::factory()->has( Profile::factory(), 'profile' )->create(); $postNoSetting = Post::factory()->create(['author_id' => $userWithProfileNoSetting->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([$postWithSetting, $postNoSetting, $postNoProfile, $postNoAuthor]); // Just verify sorting doesn't crash with nullable nested relationships livewire(PostsTable::class) ->sortTable('author.setting.theme') ->assertCanSeeTableRecords($allPosts) ->sortTable('author.setting.theme', 'desc') ->assertCanSeeTableRecords($allPosts); }); it('can search records with `BelongsTo` -> `HasOneThrough` relationship', function (): void { $searchTheme = 'unique-post-author-theme'; $matchingUser = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['theme' => $searchTheme]), 'setting' ), 'profile' )->create(); $matchingPost = Post::factory()->for($matchingUser, 'author')->create(); $nonMatchingPosts = Post::factory()->count(3)->create(); livewire(PostsTable::class) ->searchTable($searchTheme) ->assertCanSeeTableRecords([$matchingPost]) ->assertCanNotSeeTableRecords($nonMatchingPosts); }); it('can sort records with `HasOneThrough` -> `BelongsTo` relationship', function (): void { $languageNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon']; foreach ($languageNames as $languageName) { $language = Language::factory()->create(['name' => $languageName]); User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language_id' => $language->id]), 'setting' ), 'profile' )->create(); } $sortedAsc = User::query() ->orderBy( Language::query() ->select('languages.name') ->join('settings', 'settings.language_id', '=', 'languages.id') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); $sortedDesc = User::query() ->orderByDesc( Language::query() ->select('languages.name') ->join('settings', 'settings.language_id', '=', 'languages.id') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); livewire(UsersTable::class) ->sortTable('setting.language.name') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('setting.language.name', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with nullable `HasOneThrough` -> `BelongsTo` relationship', function (): void { // User with full chain $language = Language::factory()->create(['name' => 'English']); $userWithLanguage = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language_id' => $language->id]), 'setting' ), 'profile' )->create(); // User with profile and setting but no language $userWithSettingNoLanguage = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language_id' => null]), 'setting' ), 'profile' )->create(); // User with profile but no setting $userWithProfileNoSetting = User::factory()->has( Profile::factory(), 'profile' )->create(); // User without profile $userWithoutProfile = User::factory()->create(); $allUsers = collect([$userWithLanguage, $userWithSettingNoLanguage, $userWithProfileNoSetting, $userWithoutProfile]); livewire(UsersTable::class) ->sortTable('setting.language.name') ->assertCanSeeTableRecords($allUsers) ->sortTable('setting.language.name', 'desc') ->assertCanSeeTableRecords($allUsers); }); it('can sort records with `BelongsTo` -> `HasOneThrough` -> `BelongsTo` relationship', function (): void { $languageNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon']; foreach ($languageNames as $languageName) { $language = Language::factory()->create(['name' => $languageName]); $user = User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language_id' => $language->id]), 'setting' ), 'profile' )->create(); Post::factory()->create(['author_id' => $user->id]); } $sortedAsc = Post::query() ->orderBy( Language::query() ->select('languages.name') ->join('settings', 'settings.language_id', '=', 'languages.id') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->join('users', 'users.id', '=', 'profiles.user_id') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); $sortedDesc = Post::query() ->orderByDesc( Language::query() ->select('languages.name') ->join('settings', 'settings.language_id', '=', 'languages.id') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->join('users', 'users.id', '=', 'profiles.user_id') ->whereColumn('users.id', 'posts.author_id') ->limit(1) ) ->orderBy('posts.id') ->get(); livewire(PostsTable::class) ->sortTable('author.setting.language.name') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('author.setting.language.name', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with `HasOne` relationship that uses a custom `where()` constraint', function (): void { // User 1: has both published and unpublished posts; should sort by published $user1 = User::factory()->create(); Post::factory()->create(['author_id' => $user1->id, 'is_published' => false, 'title' => 'Z Unpublished']); Post::factory()->create(['author_id' => $user1->id, 'is_published' => true, 'title' => 'A Published']); // User 2: only has unpublished posts; should sort as null $user2 = User::factory()->create(); Post::factory()->create(['author_id' => $user2->id, 'is_published' => false, 'title' => 'B Unpublished']); // User 3: has a published post $user3 = User::factory()->create(); Post::factory()->create(['author_id' => $user3->id, 'is_published' => true, 'title' => 'M Published']); $sortedAsc = User::query() ->orderBy( Post::query() ->select('posts.title') ->whereColumn('posts.author_id', 'users.id') ->where('posts.is_published', true) ->limit(1) ) ->orderBy('users.id') ->get(); $sortedDesc = User::query() ->orderByDesc( Post::query() ->select('posts.title') ->whereColumn('posts.author_id', 'users.id') ->where('posts.is_published', true) ->limit(1) ) ->orderBy('users.id') ->get(); livewire(UsersTable::class) ->sortTable('publishedPost.title') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('publishedPost.title', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); it('can sort records with `HasOneThrough` -> `BelongsTo` relationship that uses `withTrashed()` when the related model is soft-deleted', function (): void { $alphaLanguage = Language::factory()->create(['name' => 'Alpha']); $betaLanguage = Language::factory()->create(['name' => 'Beta']); $gammaLanguage = Language::factory()->create(['name' => 'Gamma']); foreach ([$alphaLanguage, $betaLanguage, $gammaLanguage] as $language) { User::factory()->has( Profile::factory()->has( Setting::factory()->state(['language_id' => $language->id]), 'setting' ), 'profile' )->create(); } // Soft-delete the Gamma language $gammaLanguage->delete(); $sortedAsc = User::query() ->orderBy( Language::query() ->withTrashed() ->select('languages.name') ->join('settings', 'settings.language_id', '=', 'languages.id') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); $sortedDesc = User::query() ->orderByDesc( Language::query() ->withTrashed() ->select('languages.name') ->join('settings', 'settings.language_id', '=', 'languages.id') ->join('profiles', 'profiles.id', '=', 'settings.profile_id') ->whereColumn('profiles.user_id', 'users.id') ->limit(1) ) ->orderBy('users.id') ->get(); livewire(UsersTable::class) ->sortTable('setting.languageWithTrashed.name') ->assertCanSeeTableRecords($sortedAsc, inOrder: true) ->sortTable('setting.languageWithTrashed.name', 'desc') ->assertCanSeeTableRecords($sortedDesc, inOrder: true); }); }); it('can search records using table-level searchable columns', function (): void { $posts = Post::factory()->count(10)->create(); $title = $posts->first()->title; livewire(PostsTableWithTableSearchableColumns::class) ->searchTable($title) ->assertCanSeeTableRecords($posts->where('title', $title)) ->assertCanNotSeeTableRecords($posts->where('title', '!=', $title)); }); it('can search records using table-level searchable columns with relationship', function (): void { $posts = Post::factory()->count(10)->create(); $author = $posts->first()->author->name; livewire(PostsTableWithTableSearchableColumns::class) ->searchTable($author) ->assertCanSeeTableRecords($posts->where('author.name', $author)) ->assertCanNotSeeTableRecords($posts->where('author.name', '!=', $author)); }); it('can search records using table-level searchable columns case insensitively', function (): void { $matchingPost = Post::factory()->create([ 'title' => 'Test Post Title', ]); $nonMatchingPosts = Post::factory()->count(3)->create(); livewire(PostsTableWithTableSearchableColumns::class) ->searchTable('test post title') ->assertCanSeeTableRecords([$matchingPost]) ->assertCanNotSeeTableRecords($nonMatchingPosts); }); it('can search records using table-level searchable columns with relationship case insensitively', function (): void { $matchingAuthor = User::factory()->create(['name' => 'John Smith']); $matchingPosts = Post::factory() ->for($matchingAuthor, 'author') ->count(3) ->create(); $nonMatchingPosts = Post::factory()->count(3)->create(); livewire(PostsTableWithTableSearchableColumns::class) ->searchTable('john smith') ->assertCanSeeTableRecords($matchingPosts) ->assertCanNotSeeTableRecords($nonMatchingPosts); });