create(['handle' => 'octocat', 'preferred_lane' => 4]);
$viewer = User::factory()->create();
$user->followers()->attach($viewer);
$user->following()->attach(User::factory()->count(2)->create());
Commit::factory()->for($user)->create(['created_at' => now()->subDay(), 'body' => 'Older']);
Commit::factory()->for($user)->create(['body' => 'Newer']);
Commit::factory()->create();
$response = $this->actingAs($viewer)
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->component('user/show')
->where('user.id', $user->id)
->where('user.handle', 'octocat')
->where('user.lane', 4)
->where('user.commits_count', 2)
->where('user.followers_count', 1)
->where('user.following_count', 2)
->where('user.has_sponsors_listing', false)
->missing('user.email')
->missing('user.github_id')
->where('isFollowing', true)
->where('followsYou', false)
->has('achievements')
->missing('xpLog')
->has('commits.data', 2)
->where('commits.data.0.body', 'Newer')
->where('commits.data.0.user.handle', 'octocat')
->where('commits.data.0.stars_count', 0)
->where('commits.data.0.starred', false)
->where('commits.data.1.body', 'Older'));
});
it('only lets guests see the local time of users who opted in', function (): void {
User::factory()->create(['handle' => 'shared', 'timezone' => 'Europe/Berlin', 'show_local_time' => true]);
User::factory()->create(['handle' => 'private', 'timezone' => 'Europe/Berlin']);
$this->get('/@shared')
->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
->where('user.timezone', 'Europe/Berlin')
->where('user.show_local_time', true));
$this->get('/@private')
->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
->where('user.timezone', 'Europe/Berlin')
->where('user.show_local_time', false));
});
it('renders whether the user can be sponsored on github', function (): void {
$user = User::factory()->create(['has_sponsors_listing' => true]);
$response = $this->actingAs(User::factory()->create())
->get(route('user.show', $user));
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('user.has_sponsors_listing', true));
});
it('does not count the user relations or activity when reloading only commits', function (string $tab): void {
$user = User::factory()->has(Commit::factory())->create(['handle' => 'octocat']);
Commit::factory()->for($user)->for(Commit::factory(), 'parent')->create();
$response = $this->actingAs(User::factory()->create())
->get(route('user.show', ['user' => $user, 'tab' => $tab]));
$response->assertOk()
->assertInertia(function ($page): void {
$page->where('user.commits_count', 2);
DB::enableQueryLog();
$page->reloadOnly('commits', fn ($reload) => $reload
->missing('user')
->missing('isFollowing')
->missing('followsYou')
->missing('tab')
->missing('activity')
->missing('achievements'));
});
expect(collect(DB::getQueryLog())
->pluck('query')
->reject(fn (string $query): bool => str_starts_with($query, 'select "commits"."id", "commits"."user_id"') || str_starts_with($query, 'select * from (select "commits"."id"'))
->all())
->not->toBeEmpty()
->each(fn ($query) => $query->not->toContain('count(*)')->not->toContain('exists'));
})->with(['commits', 'replies']);
it('does not count the profile of guests reloading only commits', function (): void {
User::factory()->has(Commit::factory())->create(['handle' => 'octocat']);
DB::enableQueryLog();
$this->get('/@octocat', [
'X-Inertia' => 'true',
'X-Inertia-Version' => (string) new HandleInertiaRequests()->version(request()),
'X-Inertia-Partial-Component' => 'user/show',
'X-Inertia-Partial-Data' => 'commits',
])
->assertOk()
->assertJsonCount(1, 'props.commits.data')
->assertJsonMissingPath('props.user');
expect(collect(DB::getQueryLog())
->pluck('query')
->reject(fn (string $query): bool => str_starts_with($query, 'select "commits"."id", "commits"."user_id"') || str_starts_with($query, 'select * from (select "commits"."id"'))
->all())
->not->toBeEmpty()
->each(fn ($query) => $query->not->toContain('count(*)'));
});
it('counts the profile of guests once for both the page and its link preview', function (): void {
$user = User::factory()->has(Commit::factory())->create(['handle' => 'octocat']);
$user->followers()->attach(User::factory()->create());
DB::enableQueryLog();
$this->get('/@octocat')
->assertOk()
->assertInertia(fn ($page) => $page
->where('user.commits_count', 1)
->where('user.followers_count', 1))
->assertSee('"userInteractionCount":1', false);
expect(collect(DB::getQueryLog())
->pluck('query')
->filter(fn (string $query): bool => str_starts_with($query, 'select count(*) as "aggregate" from "follows" where "follows"."followed_id"')))
->toHaveCount(1);
});
it('renders the user commit activity for the last year', function (): void {
$this->travelTo(now()->setDate(2026, 10, 15)->setTime(12, 0));
$user = User::factory()->create(['handle' => 'octocat']);
Commit::factory()->for($user)->count(2)->create();
Commit::factory()->for($user)->create(['created_at' => now()->subDays(3)]);
Commit::factory()->for($user)->create(['created_at' => now()->subYears(2)]);
Commit::factory()->create();
$response = $this->actingAs(User::factory()->create())
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('activity.scale', 'year')
->has('activity.buckets', 369)
->where('activity.buckets.0', ['date' => '2025-10-12', 'count' => 0])
->where('activity.buckets.365', ['date' => '2026-10-12', 'count' => 1])
->where('activity.buckets.368', ['date' => '2026-10-15', 'count' => 2]));
});
it('renders the hourly commit activity of a user who only committed today', function (): void {
$this->travelTo(now()->setDate(2026, 10, 15)->setTime(12, 0));
$user = User::factory()->create(['handle' => 'octocat', 'created_at' => now()->subMonths(3)]);
Commit::factory()->for($user)->count(2)->create();
Commit::factory()->for($user)->create(['created_at' => now()->subHours(3)]);
$response = $this->actingAs(User::factory()->create())
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('activity.scale', 'day')
->has('activity.buckets', 24)
->where('activity.buckets.20', ['date' => '2026-10-15T09:00:00Z', 'count' => 1])
->where('activity.buckets.23', ['date' => '2026-10-15T12:00:00Z', 'count' => 2]));
});
it('renders the user commit streaks alongside starred commits', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$viewer = User::factory()->create();
$commit = Commit::factory()->for($user)->create();
Commit::factory()->for($user)->create(['created_at' => now()->subDays(3)]);
Commit::factory()->for($user)->create(['created_at' => now()->subDays(4)]);
Commit::factory()->for($user)->pending()->create(['created_at' => now()->subDay()]);
$viewer->starredCommits()->attach($commit);
$response = $this->actingAs($viewer)
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('streaks', ['longest' => 2, 'current' => 1])
->where('commits.data.0.id', $commit->id)
->where('commits.data.0.stars_count', 1)
->where('commits.data.0.starred', true));
});
it('does not compute the streaks when reloading only commits', function (string $tab): void {
$user = User::factory()->has(Commit::factory())->create(['handle' => 'octocat']);
Commit::factory()->for($user)->for(Commit::factory(), 'parent')->create();
$response = $this->actingAs(User::factory()->create())
->get(route('user.show', ['user' => $user, 'tab' => $tab]));
$response->assertOk()
->assertInertia(function ($page) use ($tab): void {
$page->where('tab', $tab)
->where('streaks', ['longest' => 1, 'current' => 1]);
DB::enableQueryLog();
$page->reloadOnly('commits', fn ($reload) => $reload->missing('streaks'));
});
expect(collect(DB::getQueryLog())->pluck('query')->all())
->not->toBeEmpty()
->each->not->toContain('distinct');
})->with(['commits', 'replies']);
it('renders whether the viewer is not following the user', function (): void {
$user = User::factory()->create();
$response = $this->actingAs(User::factory()->create())
->get(route('user.show', $user));
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('isFollowing', false)
->where('isMuting', false)
->has('commits.data', 0));
});
it('still shows the commits of a muted user on their profile', function (): void {
$viewer = User::factory()->create();
$user = User::factory()->has(Commit::factory())->create();
$viewer->muting()->attach($user);
$response = $this->actingAs($viewer)
->get(route('user.show', $user));
$response->assertOk()
->assertInertia(fn ($page) => $page
->component('user/show')
->where('isMuting', true)
->has('commits.data', 1));
});
it('shows the blocker who they blocked, without any of their commits', function (): void {
$viewer = User::factory()->create();
$user = User::factory()->has(Commit::factory())->create(['handle' => 'octocat', 'pinned_commit_id' => null]);
$viewer->blocking()->attach($user);
$response = $this->actingAs($viewer)
->get(route('user.show', $user));
$response->assertOk()
->assertInertia(fn ($page) => $page
->component('user/unavailable')
->where('handle', 'octocat')
->where('blockedUser.id', $user->id)
->where('blockedUser.lane', $user->lane())
->missing('blockedUser.email')
->missing('commits')
->missing('activity'));
});
it('shows a neutral unavailable profile to someone the user blocked', function (): void {
$viewer = User::factory()->create();
$user = User::factory()->has(Commit::factory())->create(['handle' => 'octocat', 'bio' => 'Private bio']);
$user->blocking()->attach($viewer);
$response = $this->actingAs($viewer)
->get(route('user.show', ['user' => $user, 'tab' => 'replies']));
$response->assertOk()
->assertInertia(fn ($page) => $page
->component('user/unavailable')
->where('handle', 'octocat')
->where('blockedUser', null)
->missing('commits')
->missing('user'));
expect($response->getContent())->not->toContain('Private bio');
});
it('renders whether the user follows the viewer', function (): void {
$user = User::factory()->create();
$viewer = User::factory()->create();
$user->following()->attach($viewer);
$response = $this->actingAs($viewer)
->get(route('user.show', $user));
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('isFollowing', false)
->where('followsYou', true));
});
it('returns not found for unknown handles', function (): void {
$response = $this->actingAs(User::factory()->create())
->get('/@missing');
$response->assertNotFound();
});
it('shows guests the profile with its approved commits and a link preview', function (): void {
$user = User::factory()->create([
'handle' => 'octocat',
'name' => 'The Octocat',
'bio' => 'I am the Octocat.',
]);
Commit::factory()->for($user)->create(['body' => 'A commit everyone may read']);
Commit::factory()->for($user)->pending()->create(['body' => 'A pending commit only its author may read']);
$response = $this->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->component('user/show')
->where('user.commits_count', 1)
->where('isFollowing', false)
->where('isMuting', false)
->has('commits.data', 1)
->where('commits.data.0.body', 'A commit everyone may read')
->where('streaks.longest', 1)
->has('activity'))
->assertSee('
The Octocat (@octocat) on /dev/null', false)
->assertSee('', false)
->assertSee('', false)
->assertSee('"@type":"ProfilePage"', false)
->assertSee('"sameAs":["https://github.com/octocat"', false)
->assertSee('', false)
->assertSee('', false)
->assertSee('', false)
->assertSee('', false)
->assertSee('', false)
->assertSee('', false)
->assertDontSee(url('/og.png'))
->assertDontSee('A pending commit only its author may read');
$this->assertGuest();
});
it('describes guests a profile without a bio', function (): void {
User::factory()->create(['handle' => 'octocat', 'name' => 'octocat', 'bio' => null]);
$this->get('/@octocat')
->assertOk()
->assertSee('', false)
->assertSee('', false);
});
it('escapes the profile in the link preview', function (): void {
User::factory()->create([
'handle' => 'octocat',
'name' => '">',
'bio' => '
',
]);
$this->get('/@octocat')
->assertOk()
->assertDontSee('', false)
->assertDontSee('
assertSee('"><script>alert(1)</script> (@octocat) on /dev/null', false);
});
it('changes the link preview image version only when public profile data changes', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$version = fn (): string => Str::betweenFirst($this->get('/@octocat')->getContent(), 'og.png?v=', '"');
$initial = $version();
Commit::factory()->for($user)->pending()->create();
Commit::factory()->for($user)->rejected()->create();
expect($version())->toBe($initial);
Commit::factory()->for($user)->create();
expect($version())->not->toBe($initial);
});
it('changes the link preview image version only when something drawn on it changes', function (): void {
$user = User::factory()->create(['handle' => 'octocat', 'log' => Log::Following]);
$version = fn (): string => Str::betweenFirst($this->get('/@octocat')->getContent(), 'og.png?v=', '"');
$initial = $version();
$this->travel(1)->minute();
$user->update(['log' => Log::Trending]);
expect($version())->toBe($initial);
$user->profile->update(['name' => 'The Octocat']);
expect($version())->not->toBe($initial);
});
it('returns guests to the profile after signing in', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$this->get('/@octocat')->assertOk();
expect(session('url.intended'))->toBe(route('user.show', $user));
});
it('does not return guests to a profile they only prefetched or partially reloaded', function (): void {
User::factory()->create(['handle' => 'octocat']);
$this->get('/@octocat', ['Purpose' => 'prefetch'])->assertOk();
$this->get('/@octocat', [
'X-Inertia' => 'true',
'X-Inertia-Version' => (string) new HandleInertiaRequests()->version(request()),
'X-Inertia-Partial-Component' => 'user/show',
'X-Inertia-Partial-Data' => 'commits',
])->assertOk();
expect(session('url.intended'))->toBeNull();
});
it('does not change where signed-in users return to', function (): void {
User::factory()->create(['handle' => 'octocat']);
$this->actingAs(User::factory()->create())->get('/@octocat')->assertOk();
expect(session('url.intended'))->toBeNull();
});
it('returns not found for unknown handles to guests', function (): void {
$this->get('/@missing')->assertNotFound();
});
it('keeps the default link preview for signed-in users', function (): void {
User::factory()->create(['handle' => 'octocat', 'name' => 'The Octocat']);
$this->actingAs(User::factory()->create())
->get('/@octocat')
->assertOk()
->assertInertia(fn ($page) => $page->component('user/show'))
->assertSee('', false)
->assertDontSee('og:url', false)
->assertDontSee('application/atom+xml', false)
->assertDontSee('application/ld+json', false);
});
it('may delete user account', function (): void {
$user = User::factory()->create();
Commit::factory()->for($user)->create();
$response = $this->actingAs($user)
->fromRoute('user-profile.edit')
->delete(route('user.destroy'));
$response->assertRedirectToRoute('home');
expect($user->fresh())->toBeNull()
->and(Commit::query()->count())->toBe(0);
$this->assertGuest();
});
it('keeps the replies other people left under the replies of a deleted account', function (): void {
$user = User::factory()->create();
$root = Commit::factory()->create();
$reply = Commit::factory()->for($user)->for($root, 'parent')->create();
$otherReply = Commit::factory()->for($reply, 'parent')->create();
$this->actingAs($user)->delete(route('user.destroy'));
$response = $this->actingAs(User::factory()->create())
->get(route('commit.show', [$otherReply->user, $otherReply]));
$response->assertOk()
->assertInertia(fn ($page) => $page
->has('ancestors', 2)
->where('ancestors.0.id', $root->id)
->where('ancestors.1.id', $reply->id)
->missing('ancestors.1.body')
->missing('ancestors.1.user'));
$this->actingAs($root->user)
->getJson(route('commit-reply.index', $root))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $reply->id)
->assertJsonPath('data.0.replies_count', 1)
->assertJsonMissingPath('data.0.user');
$this->getJson(route('api-commit.show', $reply))->assertNotFound();
$this->getJson(route('api-commit.show', $otherReply))
->assertOk()
->assertJsonPath('data.id', $otherReply->id)
->assertJsonPath('data.parent_id', null)
->assertJsonPath('data.author.id', $otherReply->user_id);
$this->getJson(route('api-user-commit.index', $otherReply->user).'?replies=1')
->assertOk()
->assertJsonPath('data.*.id', [$otherReply->id]);
$this->get(route('user-feed.show', $root->user))
->assertOk()
->assertSee("urn:uuid:{$root->id}")
->assertDontSee("urn:uuid:{$reply->id}");
$this->getJson(route('api-user.show', $user))->assertNotFound();
$this->get(route('user-feed.show', $user))->assertNotFound();
});
it('hides pending and rejected commits from other users', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
Commit::factory()->for($user)->create(['body' => 'Approved']);
Commit::factory()->for($user)->pending()->create();
Commit::factory()->for($user)->rejected()->create();
$response = $this->actingAs(User::factory()->create())
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('user.commits_count', 1)
->where('activity.buckets.23.count', 1)
->has('commits.data', 1)
->where('commits.data.0.body', 'Approved'));
});
it('shows own pending and rejected commits', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
Commit::factory()->for($user)->create();
Commit::factory()->for($user)->pending()->create();
Commit::factory()->for($user)->rejected()->create();
$response = $this->actingAs($user)
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('user.commits_count', 3)
->where('activity.buckets.23.count', 3)
->has('commits.data', 3));
});
it('hides deleted commits from profiles', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
Commit::factory()->for($user)->create(['body' => 'Kept']);
Commit::factory()->for($user)->create()->delete();
$response = $this->actingAs($user)
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->where('user.commits_count', 1)
->where('activity.scale', 'day')
->where('activity.buckets.23.count', 1)
->where('streaks', ['longest' => 1, 'current' => 1])
->has('commits.data', 1)
->where('commits.data.0.body', 'Kept'));
});
it('only shows top-level commits on profiles', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$parent = Commit::factory()->for($user)->create(['body' => 'Root']);
Commit::factory()->for($parent, 'parent')->create();
$response = $this->actingAs(User::factory()->create())
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->has('commits.data', 1)
->where('commits.data.0.body', 'Root')
->where('commits.data.0.replies_count', 1));
});
it('leaves the next commit of the author thread out of the reply count on profiles', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$parent = Commit::factory()->for($user)->create();
$next = Commit::factory()->for($user)->for($parent, 'parent')->create();
Commit::factory()->for($parent, 'parent')->create();
$this->actingAs(User::factory()->create())
->get('/@octocat')
->assertOk()
->assertInertia(fn ($page) => $page
->where('commits.data.0.continuation_id', $next->id)
->where('commits.data.0.replies_count', 1));
});
it('includes the quoted commits of the user commits', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$quoted = Commit::factory()->for(User::factory()->state(['handle' => 'taylorotwell']))->create(['body' => 'The quoted commit']);
Commit::factory()->for($user)->for($quoted, 'quotedCommit')->create();
$response = $this->actingAs(User::factory()->create())
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->has('commits.data', 1)
->where('commits.data.0.quoted_commit.body', 'The quoted commit')
->where('commits.data.0.quoted_commit.user.handle', 'taylorotwell'));
});
it('includes the link previews of the user commits', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
$linkPreview = LinkPreview::factory()->fetched()->create(['title' => 'Octocat links']);
Commit::factory()->for($user)->for($linkPreview)->create();
$response = $this->actingAs(User::factory()->create())
->get('/@octocat');
$response->assertOk()
->assertInertia(fn ($page) => $page
->has('commits.data', 1)
->where('commits.data.0.link_preview.title', 'Octocat links')
->missing('commits.data.0.link_preview.url_hash')
->missing('commits.data.0.link_preview.fetched_at'));
});
it('preloads only replies visible to the profile visitor', function (): void {
$author = User::factory()->create();
$viewer = User::factory()->create();
$muted = User::factory()->create();
$blocked = User::factory()->create();
$viewer->muting()->attach($muted);
$blocked->blocking()->attach($viewer);
$parent = Commit::factory()->for($author)->create();
$visible = Commit::factory()->for($parent, 'parent')->create(['created_at' => now()->subMinutes(2)]);
$ownPending = Commit::factory()->for($parent, 'parent')->for($viewer)->pending()->create(['created_at' => now()->subMinute()]);
Commit::factory()->for($parent, 'parent')->pending()->create();
Commit::factory()->for($parent, 'parent')->rejected()->create();
Commit::factory()->for($parent, 'parent')->create(['deleted_at' => now()]);
Commit::factory()->for($parent, 'quotedCommit')->for($muted)->create();
Commit::factory()->for($parent, 'quotedCommit')->for($blocked)->create();
$this->actingAs($viewer)->get(route('user.show', $author))
->assertOk()->assertInertia(fn ($page) => $page
->where('commits.data.0.replies_count', 2)
->has('commits.data.0.reply_preview.data', 2)
->where('commits.data.0.reply_preview.data.0.id', $ownPending->id)
->where('commits.data.0.reply_preview.data.1.id', $visible->id)
->where('commits.data.0.reply_preview.next_cursor', null)
->missing('commits.data.0.reply_preview.data.0.user.email'));
});
it('offers a remote only when the viewer could push to the profile', function (string $case, bool $canPush): void {
$viewer = User::factory()->create();
$profile = User::factory()->create();
if ($case !== 'they do not follow you') {
$profile->following()->attach($viewer);
}
match ($case) {
'you have remotes off' => $viewer->update(['remotes_enabled' => false]),
'they have remotes off' => $profile->update(['remotes_enabled' => false]),
default => null,
};
$this->actingAs($viewer)->get(route('user.show', $profile))
->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page->where('canPush', $canPush));
})->with([
['they follow you', true],
['they do not follow you', false],
['you have remotes off', false],
['they have remotes off', false],
]);
it('offers guests and the profile owner no remote', function (): void {
$user = User::factory()->create();
$this->actingAs($user)->get(route('user.show', $user))
->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page->where('canPush', false));
});
it('shows the github profile readme on the readme tab, without looking for commits', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
Commit::factory()->for($user)->create();
Http::fake([
"api.github.com/user/{$user->github_id}" => Http::response(['login' => 'octocat']),
'api.github.com/repos/octocat/octocat/readme' => Http::response(''),
]);
$this->get(route('user.show', ['user' => $user, 'tab' => 'readme']))->assertOk()->assertInertia(fn ($page) => $page
->where('tab', 'readme')
->where('readme', '')
->where('pinnedCommit', null)
->where('commits.data', []));
});
it('tells the readme tab when github did not answer', function (): void {
$user = User::factory()->create(['handle' => 'octocat']);
Http::fake(["api.github.com/user/{$user->github_id}" => Http::response(status: 502)]);
$this->get(route('user.show', ['user' => $user, 'tab' => 'readme']))->assertOk()->assertInertia(fn ($page) => $page
->where('readme', false));
});
it('does not ask github for the readme on the other tabs', function (string $tab): void {
Http::preventStrayRequests();
$user = User::factory()->create(['handle' => 'octocat']);
$this->get(route('user.show', ['user' => $user, 'tab' => $tab]))->assertOk()->assertInertia(fn ($page) => $page
->where('readme', null));
})->with(['commits', 'replies']);
it('names the followers the viewer knows, the most followed first', function (): void {
$viewer = User::factory()->create();
$user = User::factory()->create();
[$quiet, $popular, $middle, $fourth] = User::factory()->count(4)->create();
$user->followers()->attach([$quiet->id, $popular->id, $middle->id, $fourth->id, User::factory()->create()->id]);
$viewer->following()->attach([$quiet->id, $popular->id, $middle->id, $fourth->id]);
$popular->followers()->attach(User::factory()->count(3)->create());
$middle->followers()->attach(User::factory()->count(2)->create());
$fourth->followers()->attach(User::factory()->create());
$this->actingAs($viewer)
->get(route('user.show', $user))
->assertInertia(fn ($page) => $page
->where('followersYouKnow.count', 4)
->has('followersYouKnow.users', 3)
->where('followersYouKnow.users.0.id', $popular->id)
->where('followersYouKnow.users.1.id', $middle->id)
->where('followersYouKnow.users.2.id', $fourth->id));
});
it('has no followers the viewer knows when they follow none of them', function (): void {
$viewer = User::factory()->create();
$user = User::factory()->create();
$user->followers()->attach(User::factory()->create());
$this->actingAs($viewer)
->get(route('user.show', $user))
->assertInertia(fn ($page) => $page
->where('followersYouKnow.count', 0)
->has('followersYouKnow.users', 0));
});
it('names no followers the viewer knows to guests or on their own profile', function (): void {
$user = User::factory()->create();
$this->get(route('user.show', $user))
->assertInertia(fn ($page) => $page->where('followersYouKnow', null));
$this->actingAs($user)
->get(route('user.show', $user))
->assertInertia(fn ($page) => $page->where('followersYouKnow', null));
it('loads the xp log only when the achievements dialog asks for it', function (): void {
$this->travelTo(now()->setDate(2026, 10, 22)->setTime(12, 0));
$user = User::factory()->create(['handle' => 'octocat', 'created_at' => '2026-10-01 10:00:00']);
Commit::factory()->for($user)->create(['created_at' => '2026-10-21 09:00:00']);
$this->withHeaders([
'X-Inertia' => 'true',
'X-Inertia-Version' => (string) new HandleInertiaRequests()->version(request()),
'X-Inertia-Partial-Component' => 'user/show',
'X-Inertia-Partial-Data' => 'xpLog',
])->get(route('user.show', $user))->assertOk()
->assertJsonPath('props.xpLog.0.date', '2026-10-21')
->assertJsonPath('props.xpLog.0.xp', 25)
->assertJsonPath('props.xpLog.1.date', '2026-10-01')
->assertJsonPath('props.xpLog.1.entries.0.source', 'shell_shocked')
->assertJsonMissingPath('props.commits');
});