Files
construprogress/tests/Feature/IssuesEnhancementsTest.php
T
javier 3d0f4d5cad feat(issues): tipo/categoría de incidencia (defecto/seguridad/calidad/documentación/otro)
- Issue::TYPES + typeLabels() (ES) + accessors type_label/type_color; columna type
  (string, default 'other') + fillable.
- IssueForm: select "Tipo de incidencia" con validación/carga/guardado.
- IssueTable: columna Tipo (badge) + SelectFilter por tipo.
- IssueDetail: badge de tipo en la cabecera.
- Sync offline: issue.create/update aceptan type; bundle (mapIssue) lo incluye.

Tests: IssuesEnhancementsTest (create muestra el campo vía HTTP, edición persiste) +
MobileApiTest (create con type). Suite 61 passing (solo 2 pre-existentes sqlite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:30:54 +02:00

225 lines
8.7 KiB
PHP

<?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);
}
// ── 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);
}
// ── 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');
}
}