2026-06-18 12:51:41 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\Feature;
|
|
|
|
|
|
|
|
|
|
use App\Livewire\IssueChecklistManager;
|
|
|
|
|
use App\Livewire\IssueDetail;
|
|
|
|
|
use App\Livewire\IssueForm;
|
|
|
|
|
use App\Models\Feature;
|
|
|
|
|
use App\Models\Issue;
|
|
|
|
|
use App\Models\IssueChecklistTemplate;
|
|
|
|
|
use App\Models\IssueTask;
|
|
|
|
|
use App\Models\Layer;
|
|
|
|
|
use App\Models\Phase;
|
|
|
|
|
use App\Models\Project;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use App\Notifications\IssueCommentedNotification;
|
|
|
|
|
use App\Notifications\IssueStatusChangedNotification;
|
|
|
|
|
use App\Notifications\IssueTaskAssignedNotification;
|
|
|
|
|
use App\Notifications\IssueTaskOverdueNotification;
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
use Illuminate\Support\Carbon;
|
|
|
|
|
use Illuminate\Support\Facades\Notification;
|
|
|
|
|
use Livewire\Livewire;
|
|
|
|
|
use Spatie\Permission\Models\Permission;
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
class IssuesEnhancementsTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
|
|
|
|
private User $user;
|
|
|
|
|
private User $assignee;
|
|
|
|
|
private Project $project;
|
|
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
parent::setUp();
|
|
|
|
|
foreach (['view issues', 'create issues', 'edit issues', 'delete issues', 'upload media'] as $p) {
|
|
|
|
|
Permission::findOrCreate($p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->user = User::factory()->create();
|
|
|
|
|
$this->user->givePermissionTo(['view issues', 'create issues', 'edit issues', 'delete issues', 'upload media']);
|
|
|
|
|
$this->assignee = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
$this->project = Project::create([
|
|
|
|
|
'reference' => 'ENH-1',
|
|
|
|
|
'name' => 'Proyecto Enh',
|
|
|
|
|
'address' => 'Calle 1',
|
|
|
|
|
'lat' => 40.0,
|
|
|
|
|
'lng' => -3.0,
|
|
|
|
|
'start_date' => now()->toDateString(),
|
|
|
|
|
'end_date_estimated' => now()->addMonths(3)->toDateString(),
|
|
|
|
|
'status' => 'in_progress',
|
|
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
$this->project->users()->attach($this->user->id, ['role_in_project' => 'supervisor']);
|
|
|
|
|
$this->project->users()->attach($this->assignee->id, ['role_in_project' => 'worker']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function makeIssue(array $attrs = []): Issue
|
|
|
|
|
{
|
|
|
|
|
return Issue::create(array_merge([
|
|
|
|
|
'project_id' => $this->project->id,
|
|
|
|
|
'title' => 'Incidencia enh',
|
|
|
|
|
'status' => 'open',
|
|
|
|
|
'priority' => 'medium',
|
|
|
|
|
'reported_by' => $this->user->id,
|
|
|
|
|
], $attrs));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function makeFeature(): Feature
|
|
|
|
|
{
|
|
|
|
|
$phase = Phase::create([
|
|
|
|
|
'project_id' => $this->project->id, 'name' => 'F', 'order' => 1,
|
|
|
|
|
'color' => '#000', 'progress_percent' => 0,
|
|
|
|
|
]);
|
|
|
|
|
$layer = Layer::create([
|
|
|
|
|
'project_id' => $this->project->id, 'phase_id' => $phase->id,
|
|
|
|
|
'name' => 'L', 'color' => '#111', 'uploaded_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
return Feature::create([
|
|
|
|
|
'layer_id' => $layer->id, 'name' => 'Muro norte',
|
|
|
|
|
'geometry' => ['type' => 'Point', 'coordinates' => [-3.0, 40.0]],
|
|
|
|
|
'progress' => 0, 'status' => 'planned',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Notifications ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function test_assigning_a_task_notifies_the_assignee(): void
|
|
|
|
|
{
|
|
|
|
|
Notification::fake();
|
|
|
|
|
$issue = $this->makeIssue();
|
|
|
|
|
|
|
|
|
|
Livewire::actingAs($this->user)
|
|
|
|
|
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
|
|
|
|
|
->set('newTaskTitle', 'Sanear zona')
|
|
|
|
|
->set('newTaskAssignee', $this->assignee->id)
|
|
|
|
|
->call('addTask');
|
|
|
|
|
|
|
|
|
|
Notification::assertSentTo($this->assignee, IssueTaskAssignedNotification::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_commenting_notifies_the_assignee(): void
|
|
|
|
|
{
|
|
|
|
|
Notification::fake();
|
|
|
|
|
$issue = $this->makeIssue(['assigned_to' => $this->assignee->id]);
|
|
|
|
|
|
|
|
|
|
Livewire::actingAs($this->user)
|
|
|
|
|
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
|
|
|
|
|
->set('newComment', 'Revisado en obra')
|
|
|
|
|
->call('addComment');
|
|
|
|
|
|
|
|
|
|
Notification::assertSentTo($this->assignee, IssueCommentedNotification::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_resolving_notifies_stakeholders(): void
|
|
|
|
|
{
|
|
|
|
|
Notification::fake();
|
|
|
|
|
$issue = $this->makeIssue(['assigned_to' => $this->assignee->id]);
|
|
|
|
|
|
|
|
|
|
Livewire::actingAs($this->user)
|
|
|
|
|
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
|
|
|
|
|
->call('verifyResolve');
|
|
|
|
|
|
|
|
|
|
Notification::assertSentTo($this->assignee, IssueStatusChangedNotification::class);
|
|
|
|
|
$this->assertEquals('resolved', $issue->fresh()->status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Checklist templates ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function test_checklist_manager_creates_a_template(): void
|
|
|
|
|
{
|
|
|
|
|
Livewire::actingAs($this->user)
|
|
|
|
|
->test(IssueChecklistManager::class, ['project' => $this->project])
|
|
|
|
|
->set('name', 'Reparación grieta')
|
|
|
|
|
->set('items', ['Picar', 'Sellar', 'Pintar'])
|
|
|
|
|
->call('save');
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('issue_checklist_templates', [
|
|
|
|
|
'project_id' => $this->project->id, 'name' => 'Reparación grieta',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_applying_a_template_creates_the_tasks(): void
|
|
|
|
|
{
|
|
|
|
|
$issue = $this->makeIssue();
|
|
|
|
|
$template = IssueChecklistTemplate::create([
|
|
|
|
|
'project_id' => $this->project->id,
|
|
|
|
|
'name' => 'Plantilla',
|
|
|
|
|
'items' => ['Picar', 'Sellar', 'Pintar'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Livewire::actingAs($this->user)
|
|
|
|
|
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
|
|
|
|
|
->set('applyTemplateId', $template->id)
|
|
|
|
|
->call('applyTemplate');
|
|
|
|
|
|
|
|
|
|
$this->assertEquals(3, $issue->tasks()->count());
|
|
|
|
|
$this->assertEqualsCanonicalizing(['Picar', 'Sellar', 'Pintar'], $issue->tasks()->pluck('title')->all());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Overdue alerts ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function test_overdue_command_notifies_assignee_once(): void
|
|
|
|
|
{
|
|
|
|
|
Notification::fake();
|
|
|
|
|
$issue = $this->makeIssue();
|
|
|
|
|
$task = $issue->tasks()->create([
|
|
|
|
|
'title' => 'Tarea vencida',
|
|
|
|
|
'assigned_to' => $this->assignee->id,
|
|
|
|
|
'due_date' => now()->subDays(2)->toDateString(),
|
|
|
|
|
'is_done' => false,
|
|
|
|
|
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->artisan('issues:notify-overdue')->assertSuccessful();
|
|
|
|
|
$this->artisan('issues:notify-overdue')->assertSuccessful(); // idempotent
|
|
|
|
|
|
|
|
|
|
Notification::assertSentTo($this->assignee, IssueTaskOverdueNotification::class);
|
|
|
|
|
Notification::assertCount(1);
|
|
|
|
|
$this->assertNotNull($task->fresh()->overdue_notified_at);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 13:30:54 +02:00
|
|
|
// ── Issue type / category ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function test_create_form_shows_the_type_field(): void
|
|
|
|
|
{
|
|
|
|
|
$this->actingAs($this->user)
|
|
|
|
|
->get(route('projects.issues.create', $this->project))
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertSee('Tipo de incidencia')
|
|
|
|
|
->assertSee('Defecto')
|
|
|
|
|
->assertSee('Seguridad');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_editing_an_issue_persists_the_type(): void
|
|
|
|
|
{
|
|
|
|
|
$issue = $this->makeIssue(['type' => 'defect']);
|
|
|
|
|
|
|
|
|
|
Livewire::actingAs($this->user)
|
|
|
|
|
->test(IssueForm::class, ['project' => $this->project, 'issue' => $issue])
|
|
|
|
|
->assertSet('type', 'defect')
|
|
|
|
|
->set('type', 'safety')
|
|
|
|
|
->call('save');
|
|
|
|
|
|
|
|
|
|
$this->assertEquals('safety', $issue->fresh()->type);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 12:51:41 +02:00
|
|
|
// ── Report issue from a map feature ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function test_issue_form_prelinks_feature_from_query(): void
|
|
|
|
|
{
|
|
|
|
|
$feature = $this->makeFeature();
|
|
|
|
|
|
|
|
|
|
// The create form mounts during the GET request, where it reads ?feature=.
|
|
|
|
|
$this->actingAs($this->user)
|
|
|
|
|
->get(route('projects.issues.create', ['project' => $this->project, 'feature' => $feature->id]))
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertSee('Muro norte')
|
|
|
|
|
->assertSee('Vinculada al elemento del mapa');
|
|
|
|
|
}
|
|
|
|
|
}
|