Files
L-Ami-Fiduciaire/tests/Feature/Dashboard/ActivityFeedTest.php
Saad Zoubir a02b5f12d8 feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.

Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00

212 lines
6.8 KiB
PHP

<?php
use App\Enums\DeclarationStatus;
use App\Models\Client;
use App\Models\Declaration;
use App\Models\User;
use App\Models\Workspace;
function setupActivityWorkspace(string $role = 'owner'): array
{
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => $role]);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
session(['current_workspace_id' => $workspace->id]);
return [$user, $workspace, $client];
}
test('owner sees workspace-wide activity in activities prop', function () {
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
$otherUser = User::factory()->create();
$workspace->users()->attach($otherUser->id, ['role' => 'worker']);
// Act as owner BEFORE creating declaration so Spatie logs the causer
$this->actingAs($owner);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherUser->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(10),
]);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('activities')
->where('activities.0.actorName', $owner->name)
);
});
test('worker sees only activity related to their assigned declarations', function () {
[$worker, $workspace, $client] = setupActivityWorkspace('worker');
$otherWorker = User::factory()->create();
$workspace->users()->attach($otherWorker->id, ['role' => 'worker']);
// Declaration assigned to THIS worker — should appear in feed
$assignedDeclaration = Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(10),
]);
// Declaration assigned to OTHER worker — should NOT appear
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherWorker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(5),
]);
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('activities')
);
// Verify the worker only sees activities for their own declarations
$activitiesData = $response->original->getData()['page']['props']['activities'];
expect(count($activitiesData))->toBeGreaterThan(0);
// All activities should relate to the worker's assigned client
collect($activitiesData)->each(function ($a) use ($client) {
expect($a['description'])->toContain($client->company_name);
});
});
test('activity entries contain expected fields', function () {
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $owner->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(10),
]);
$this->actingAs($owner);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('activities.0', fn ($activity) => $activity
->has('id')
->has('actorName')
->has('actorInitials')
->has('description')
->has('targetUrl')
->has('targetLabel')
->has('timestamp')
->has('eventType')
)
);
});
test('feed is limited to 20 most recent events', function () {
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
// Create 25 declarations to generate 25 activity log entries
for ($i = 0; $i < 25; $i++) {
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $owner->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays($i + 1),
]);
}
$this->actingAs($owner);
$response = $this->get(route('dashboard'));
$response->assertOk();
$activitiesData = $response->original->getData()['page']['props']['activities'];
expect(count($activitiesData))->toBeLessThanOrEqual(20);
});
test('empty activity returns empty array', function () {
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner']);
session(['current_workspace_id' => $workspace->id]);
// No declarations or activity created
$this->actingAs($user);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('activities', [])
);
});
test('activity with deleted subject does not cause errors', function () {
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
// Create a declaration (logs activity), then soft-delete it
$declaration = Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $owner->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(10),
]);
$declaration->delete();
$this->actingAs($owner);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('activities')
);
});
test('manager sees workspace-wide activity same as owner', function () {
[$manager, $workspace, $client] = setupActivityWorkspace('manager');
$worker = User::factory()->create();
$workspace->users()->attach($worker->id, ['role' => 'worker']);
// Create a declaration by the worker
$this->actingAs($worker);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(10),
]);
$this->actingAs($manager);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('activities')
);
$activitiesData = $response->original->getData()['page']['props']['activities'];
expect(count($activitiesData))->toBeGreaterThan(0);
});