pases 5 ticket updates

This commit is contained in:
Mark Gregory
2026-06-23 11:10:52 +01:00
parent 2266298b6c
commit 7c4a94b5ea
12 changed files with 312 additions and 11 deletions
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\tests\Feature;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
final class TicketCreateEditorSyncFilesTest extends TestCase
{
#[Test]
public function ticket_create_editor_pushes_details_body_to_livewire_before_validation(): void
{
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
$this->assertIsString($view);
$this->assertStringContainsString('wire:model.live="body"', $view);
$this->assertStringContainsString("window.Livewire.find(livewireId).set('body', value, false)", $view);
$this->assertStringContainsString("form.addEventListener('submit', syncBody, { capture: true })", $view);
$this->assertStringContainsString("document.addEventListener('livewire:navigated', initialiseTicketCreateEditor)", $view);
$this->assertStringContainsString("editor.addEventListener('paste', () => window.setTimeout(syncBody, 0))", $view);
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketFirstResponseSlaFilesTest extends TestCase
{
/**
* Public replies must satisfy the first-response SLA instead of only incrementing the counter.
*/
public function test_public_response_updates_first_response_sla_state(): void
{
$servicePath = dirname(__DIR__, 2).'/Services/SlaService.php';
self::assertFileExists($servicePath);
$service = file_get_contents($servicePath);
self::assertIsString($service);
self::assertStringContainsString('public function recordPublicResponse(Ticket $ticket, User $actor): void', $service);
self::assertStringContainsString('markFirstResponse($ticket, $respondedAt, $actor)', $service);
self::assertStringNotContainsString('(int) $ticket->user_id !== (int) $actor->id', $service);
}
/**
* Existing tickets with public comments but a missing first_response_at are repaired on display.
*/
public function test_ticket_show_synchronises_first_response_from_existing_public_comments(): void
{
$componentPath = dirname(__DIR__, 2).'/Livewire/TicketShow.php';
$servicePath = dirname(__DIR__, 2).'/Services/SlaService.php';
$component = file_get_contents($componentPath);
$service = file_get_contents($servicePath);
self::assertIsString($component);
self::assertIsString($service);
self::assertStringContainsString('syncFirstResponseFromComments($ticket)', $component);
self::assertStringContainsString("where('is_internal', false)", $service);
self::assertStringContainsString("whereNull('tombstoned_at')", $service);
}
/**
* The SLA card should show an accurate response state instead of Awaiting response after public replies exist.
*/
public function test_sla_card_has_clear_responded_and_awaiting_public_reply_states(): void
{
$viewPath = dirname(__DIR__, 2).'/Resources/views/livewire/ticket-show.blade.php';
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('Responded', $view);
self::assertStringContainsString('Awaiting public reply', $view);
self::assertStringNotContainsString("?? 'Awaiting response'", $view);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketPublicResponseVisibilityFilesTest extends TestCase
{
/**
* Ensure the main ticket response form has an explicit public/internal visibility choice.
*/
public function test_main_ticket_response_form_has_public_visibility_control(): void
{
$viewPath = dirname(__DIR__, 2).'/Resources/views/livewire/ticket-show.blade.php';
self::assertFileExists($viewPath);
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('Visibility', $view);
self::assertStringContainsString('Public reply', $view);
self::assertStringContainsString('Internal note', $view);
self::assertStringContainsString('name="internal" value="0"', $view);
self::assertStringContainsString('name="internal" value="1"', $view);
self::assertStringContainsString('counts as the first SLA response', $view);
}
/**
* Ensure threaded responses have the same public/internal visibility choice and visible badges.
*/
public function test_threaded_responses_have_public_visibility_control_and_badges(): void
{
$partialPath = dirname(__DIR__, 2).'/Resources/views/partials/comment-thread.blade.php';
self::assertFileExists($partialPath);
$partial = file_get_contents($partialPath);
self::assertIsString($partial);
self::assertStringContainsString('Public reply', $partial);
self::assertStringContainsString('Internal note', $partial);
self::assertStringContainsString('name="internal" value="0" checked', $partial);
self::assertStringContainsString('name="internal" value="1"', $partial);
self::assertStringContainsString('>Public</span>', $partial);
self::assertStringContainsString('>Internal</span>', $partial);
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketSlaOperationalDetailFilesTest extends TestCase
{
/**
* SLA cards must show operationally useful first-response detail.
*/
public function test_first_response_sla_card_shows_duration_responder_and_timestamp(): void
{
$componentPath = dirname(__DIR__, 2).'/Livewire/TicketShow.php';
$viewPath = dirname(__DIR__, 2).'/Resources/views/livewire/ticket-show.blade.php';
$component = file_get_contents($componentPath);
$view = file_get_contents($viewPath);
self::assertIsString($component);
self::assertIsString($view);
self::assertStringContainsString('firstResponseSlaDetail', $component);
self::assertStringContainsString('Responded in ', $component);
self::assertStringContainsString('firstResponderName', $component);
self::assertStringContainsString('Responder', $view);
self::assertStringContainsString('Responded at', $view);
}
/**
* SLA cards should use clear met, due-soon and breached states.
*/
public function test_sla_cards_have_colour_coded_state_helpers(): void
{
$componentPath = dirname(__DIR__, 2).'/Livewire/TicketShow.php';
$viewPath = dirname(__DIR__, 2).'/Resources/views/livewire/ticket-show.blade.php';
$component = file_get_contents($componentPath);
$view = file_get_contents($viewPath);
self::assertIsString($component);
self::assertIsString($view);
self::assertStringContainsString('firstResponseIsDueSoon', $component);
self::assertStringContainsString('resolutionIsDueSoon', $component);
self::assertStringContainsString('firstResponseSlaClasses', $view);
self::assertStringContainsString('resolutionSlaClasses', $view);
self::assertStringContainsString('border-amber-300', $component);
self::assertStringContainsString('border-red-300', $component);
self::assertStringContainsString('border-emerald-200', $component);
}
/**
* The ticket page should expose an SLA audit trail for support managers.
*/
public function test_sla_audit_trail_is_visible_on_ticket_page(): void
{
$servicePath = dirname(__DIR__, 2).'/Services/SlaService.php';
$viewPath = dirname(__DIR__, 2).'/Resources/views/livewire/ticket-show.blade.php';
$service = file_get_contents($servicePath);
$view = file_get_contents($viewPath);
self::assertIsString($service);
self::assertIsString($view);
self::assertStringContainsString('SLA audit trail', $view);
self::assertStringContainsString('$ticket->slaEvents->take(5)', $view);
self::assertStringContainsString('actor_name', $service);
self::assertStringContainsString('response_seconds', $service);
self::assertStringContainsString('resolved_seconds', $service);
}
/**
* Public reply counts must include threaded public replies, not only root comments.
*/
public function test_public_reply_count_uses_all_ticket_comments(): void
{
$componentPath = dirname(__DIR__, 2).'/Livewire/TicketShow.php';
$component = file_get_contents($componentPath);
self::assertIsString($component);
self::assertStringContainsString('Comment::query()', $component);
self::assertStringContainsString("where('ticket_id', \$ticket->id)", $component);
self::assertStringContainsString("\$comments->where('is_internal', false)->count()", $component);
self::assertStringNotContainsString('$ticket->comments()
->active()', $component);
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketTimerButtonSizeFilesTest extends TestCase
{
/**
* Ensure the inactive timer action remains readable, single-line, and does not reintroduce unused script files.
*/
public function test_start_timer_button_uses_readable_single_line_professional_classes(): void
{
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-timer.blade.php';
self::assertFileExists($viewPath);
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('data-dusk="start-ticket-timer"', $view);
self::assertStringContainsString('min-w-[9.5rem]', $view);
self::assertStringContainsString('whitespace-nowrap rounded-md bg-blue-600 px-4 py-2 text-sm', $view);
self::assertStringNotContainsString('px-3 py-1.5 text-xs', $view);
self::assertStringNotContainsString('ticket-timer.js', $view);
}
/**
* Ensure the active timer action also remains readable and single-line.
*/
public function test_end_timer_button_uses_matching_readable_single_line_professional_classes(): void
{
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-timer.blade.php';
self::assertFileExists($viewPath);
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('data-dusk="end-ticket-timer"', $view);
self::assertStringContainsString('min-w-[9.5rem] shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-red-600 px-4 py-2 text-sm', $view);
self::assertStringNotContainsString('rounded-lg bg-red-600 px-4 py-2.5 text-sm', $view);
self::assertStringNotContainsString('ticket-timer.js', $view);
}
}
@@ -29,6 +29,19 @@ final class TicketReferenceServiceTest extends TestCase
self::assertSame('HD-'.now()->format('Y').'-00042', $reference);
}
public function test_it_does_not_reuse_soft_deleted_ticket_references(): void
{
$year = now()->format('Y');
$ticket = $this->ticket(['reference' => 'HD-'.$year.'-00002']);
$ticket->delete();
$reference = app(TicketReferenceService::class)->next();
self::assertSame('HD-'.$year.'-00003', $reference);
}
public function test_it_uses_the_highest_reference_suffix_instead_of_latest_database_id(): void
{
$year = now()->format('Y');