Merge branch 'issue/Company_Dusk_Failure'

# Conflicts:
#	Modules/Helpdesk/changelog.yaml
This commit is contained in:
Mark Gregory
2026-07-23 14:02:32 +01:00
116 changed files with 4618 additions and 4799 deletions
@@ -48,6 +48,44 @@ final class KanbanBoardTest extends TestCase
->assertSee('High');
}
/** @test */
public function test_ticket_can_be_moved_to_another_board(): void
{
$lookups = $this->seedLookupData();
$user = $this->admin();
$boardA = $lookups['board'];
$boardB = KanbanBoardModel::query()->create([
'name' => 'Regression Target Board',
'slug' => 'regression-target-board',
'is_default' => false,
'sort_order' => 50,
]);
$boardB->statuses()->sync([
$lookups['open']->id => ['sort_order' => 10],
]);
$ticket = $this->ticket([
'owner' => $user,
'status_id' => $lookups['open']->id,
'priority_id' => $lookups['normal']->id,
'board_id' => $boardA->id,
]);
$this->actingAs($user);
Livewire::test(KanbanBoard::class)
->call('moveTicketToBoard', $ticket->id, $boardB->id)
->assertHasNoErrors();
$ticket->refresh();
$this->assertSame($boardB->id, $ticket->board_id);
$this->assertSame($lookups['open']->id, $ticket->status_id);
}
/** @test */
public function kanban_board_can_move_ticket_to_closed_status(): void
{
@@ -29,6 +29,12 @@ final class TicketBranchCommandUiFilesTest extends TestCase
self::assertStringContainsString('data-helpdesk-branch-command', $view);
self::assertStringContainsString('git checkout -b bugfix/{{ $ticket->reference }}-', $view);
self::assertStringContainsString('data-helpdesk-branch-suffix', $view);
self::assertStringContainsString('data-helpdesk-branch-prefix-select', $view);
self::assertStringContainsString('normaliseHelpdeskBranchPrefix', $view);
self::assertStringContainsString('feature/', $view);
self::assertStringContainsString('bugfix/', $view);
self::assertStringContainsString('hotfix/', $view);
self::assertStringContainsString('support/', $view);
self::assertStringContainsString('data-helpdesk-copy-branch-command', $view);
self::assertStringContainsString('copyHelpdeskBranchCommand', $view);
self::assertStringContainsString('normaliseHelpdeskBranchSuffix', $view);
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use App\Models\Company;
use Modules\Company\Models\Company;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use Tests\TestCase;
final class TicketCreateAttachmentBrowserSafetyTest extends TestCase
{
public function test_ticket_create_uses_standard_livewire_temporary_uploads(): void
{
$view = file_get_contents(dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-create.blade.php');
self::assertIsString($view);
self::assertStringContainsString('wire:submit.prevent="save"', $view);
self::assertStringContainsString('type="file"', $view);
self::assertStringContainsString('wire:model="attachments"', $view);
self::assertStringContainsString('multiple', $view);
self::assertStringContainsString('wire:key="ticket-create-attachments-input"', $view);
}
public function test_ticket_create_does_not_mix_existing_ticket_upload_behaviour_into_create_form(): void
{
$view = file_get_contents(dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-create.blade.php');
self::assertIsString($view);
self::assertStringNotContainsString('@vite([\'resources/css/app.css\', \'resources/js/app.js\'])', $view);
self::assertStringNotContainsString('helpdeskAttachmentDropzone', $view);
self::assertStringNotContainsString('DataTransfer', $view);
self::assertStringNotContainsString('$wire.uploadMultiple', $view);
self::assertStringNotContainsString('input.files =', $view);
self::assertStringNotContainsString('helpdesk-editor-files-pasted', $view);
}
public function test_ticket_create_shows_uploaded_temporary_file_names_from_livewire_property(): void
{
$view = file_get_contents(dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-create.blade.php');
self::assertIsString($view);
self::assertStringContainsString('@if (count($attachments) > 0)', $view);
self::assertStringContainsString('$attachment->getClientOriginalName()', $view);
self::assertStringContainsString('wire:loading wire:target="attachments"', $view);
}
}
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Modules\Helpdesk\Livewire\TicketCreate;
use Modules\Helpdesk\Models\Attachment;
use Modules\Helpdesk\Models\Ticket;
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
use Tests\TestCase;
final class TicketCreateAttachmentPipelineTest extends TestCase
{
use BuildsHelpdeskFixtures;
use RefreshDatabase;
public function test_ticket_create_persists_a_single_attachment_through_livewire(): void
{
Storage::fake('local');
config()->set('helpdesk.disk', 'local');
$fixtures = $this->seedLookupData();
$user = $this->user(['name' => 'Attachment Create User']);
$file = UploadedFile::fake()->create('warehouse-error.log', 12, 'text/plain');
Livewire::actingAs($user)
->test(TicketCreate::class)
->set('subject', 'Create ticket with one attachment')
->set('body', '<p>This ticket must persist a Livewire upload attachment.</p>')
->set('board_id', $fixtures['board']->id)
->set('priority_id', $fixtures['high']->id)
->set('category_id', $fixtures['category']->id)
->set('team_id', $fixtures['team']->id)
->set('project_id', $fixtures['project']->id)
->set('attachments', [$file])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('helpdesk.tickets.index'));
$ticket = Ticket::query()
->where('subject', 'Create ticket with one attachment')
->firstOrFail();
$attachment = Attachment::query()
->where('ticket_id', $ticket->id)
->whereNull('comment_id')
->firstOrFail();
self::assertSame($user->id, (int) $attachment->user_id);
self::assertSame('local', $attachment->disk);
self::assertSame('warehouse-error.log', $attachment->original_name);
self::assertSame('log', $attachment->extension());
Storage::disk('local')->assertExists($attachment->path);
}
public function test_ticket_create_persists_multiple_attachments_through_livewire(): void
{
Storage::fake('local');
config()->set('helpdesk.disk', 'local');
$fixtures = $this->seedLookupData();
$user = $this->user(['name' => 'Multiple Attachment User']);
Livewire::actingAs($user)
->test(TicketCreate::class)
->set('subject', 'Create ticket with multiple attachments')
->set('body', '<p>This ticket must persist every selected Livewire upload.</p>')
->set('board_id', $fixtures['board']->id)
->set('priority_id', $fixtures['normal']->id)
->set('category_id', $fixtures['category']->id)
->set('team_id', $fixtures['team']->id)
->set('project_id', $fixtures['project']->id)
->set('attachments', [
UploadedFile::fake()->create('first-proof.pdf', 20, 'application/pdf'),
UploadedFile::fake()->create('second-proof.txt', 8, 'text/plain'),
UploadedFile::fake()->create('third-proof.csv', 6, 'text/csv'),
])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('helpdesk.tickets.index'));
$ticket = Ticket::query()
->where('subject', 'Create ticket with multiple attachments')
->firstOrFail();
self::assertSame(3, Attachment::query()->where('ticket_id', $ticket->id)->whereNull('comment_id')->count());
foreach (['first-proof.pdf', 'second-proof.txt', 'third-proof.csv'] as $name) {
$attachment = Attachment::query()
->where('ticket_id', $ticket->id)
->where('original_name', $name)
->firstOrFail();
Storage::disk('local')->assertExists($attachment->path);
}
}
public function test_ticket_create_accepts_source_code_attachments_through_livewire(): void
{
Storage::fake('local');
config()->set('helpdesk.disk', 'local');
$fixtures = $this->seedLookupData();
$user = $this->user(['name' => 'Source Attachment User']);
Livewire::actingAs($user)
->test(TicketCreate::class)
->set('subject', 'Create ticket with source attachments')
->set('body', '<p>This ticket must accept source files by extension.</p>')
->set('board_id', $fixtures['board']->id)
->set('priority_id', $fixtures['normal']->id)
->set('category_id', $fixtures['category']->id)
->set('team_id', $fixtures['team']->id)
->set('project_id', $fixtures['project']->id)
->set('attachments', [
UploadedFile::fake()->create('TicketCreate.php', 10, 'text/plain'),
UploadedFile::fake()->create('upload-widget.js', 10, 'text/plain'),
UploadedFile::fake()->create('warehouse-sync.rpgle', 10, 'text/plain'),
])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('helpdesk.tickets.index'));
$ticket = Ticket::query()
->where('subject', 'Create ticket with source attachments')
->firstOrFail();
foreach (['TicketCreate.php', 'upload-widget.js', 'warehouse-sync.rpgle'] as $name) {
$attachment = Attachment::query()
->where('ticket_id', $ticket->id)
->where('original_name', $name)
->firstOrFail();
self::assertTrue($attachment->isSourceCode(), $name . ' should be categorised as source code.');
Storage::disk('local')->assertExists($attachment->path);
}
}
}
@@ -0,0 +1,70 @@
<?php
/**
* TicketDeveloperActionsUiFilesTest.php
*
* Guards the ticket-level developer actions workflow against UI regressions.
*/
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketDeveloperActionsUiFilesTest extends TestCase
{
/**
* Ensure the Git activity card has been promoted to a complete developer actions panel.
*/
public function test_ticket_show_exposes_developer_actions_panel(): void
{
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('data-helpdesk-developer-actions', $view);
self::assertStringContainsString('Developer actions', $view);
self::assertStringContainsString('Open repo', $view);
self::assertStringContainsString('Open branch', $view);
self::assertStringContainsString('Pull requests', $view);
self::assertStringContainsString('Activity status', $view);
}
/**
* Ensure the panel keeps copyable GitFlow branch, commit and pull request templates.
*/
public function test_ticket_show_exposes_copyable_gitflow_templates(): void
{
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('Branch create command', $view);
self::assertStringContainsString('\'git checkout -b bugfix/\' . $ticket->reference . \'-\'', $view);
self::assertStringContainsString('data-helpdesk-branch-prefix-select', $view);
self::assertStringContainsString('$gitflowPrefixes', $view);
self::assertStringContainsString('normaliseHelpdeskBranchPrefix', $view);
self::assertStringContainsString('Commit message', $view);
self::assertStringContainsString('Pull request title', $view);
self::assertStringContainsString('data-helpdesk-copy-template', $view);
self::assertStringContainsString('data-helpdesk-copy-template-button', $view);
self::assertStringContainsString('copyHelpdeskTemplate', $view);
self::assertStringContainsString('copyTextToClipboard', $view);
}
/**
* Ensure linked Git activity still renders branch and commit badges instead of being hidden by the new panel.
*/
public function test_ticket_show_still_renders_linked_git_activity_rows(): void
{
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
$view = file_get_contents($viewPath);
self::assertIsString($view);
self::assertStringContainsString('@forelse ($gitActivity as $commit)', $view);
self::assertStringContainsString('$isBranchActivity = $commit->short_hash === \'branch\';', $view);
self::assertStringContainsString('{{ $isBranchActivity ? \'Branch\' : \'Commit\' }}', $view);
self::assertStringContainsString('No Git activity linked.', $view);
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketIndexDirectUserNPlusOneFilesTest extends TestCase
{
public function test_ticket_index_eager_loads_required_relations(): void
{
$source = file_get_contents(dirname(__DIR__, 2).'/Livewire/TicketIndex.php');
self::assertIsString($source);
self::assertStringContainsString("->with(['status', 'priority', 'category', 'project', 'team'])", $source);
self::assertStringNotContainsString("'user:id,name,email'", $source);
self::assertStringNotContainsString("'owner:id,name,email'", $source);
self::assertStringNotContainsString("'agent:id,name,email'", $source);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketIndexUserNPlusOneFilesTest extends TestCase
{
public function test_ticket_index_uses_scalar_agent_name(): void
{
$component = file_get_contents(dirname(__DIR__, 2).'/Livewire/TicketIndex.php');
$view = file_get_contents(dirname(__DIR__, 2).'/Resources/views/livewire/ticket-index.blade.php');
self::assertIsString($component);
self::assertIsString($view);
self::assertStringContainsString("->select('helpdesk_tickets.*')", $component);
self::assertStringContainsString("'agent_name' => User::query()", $component);
self::assertStringContainsString("->whereColumn('users.id', 'helpdesk_tickets.agent_id')", $component);
self::assertStringContainsString('$ticket->agent_name ?? \'Unassigned\'', $view);
self::assertStringNotContainsString('$ticket->agent?->name ?? \'Unassigned\'', $view);
}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use Tests\TestCase;
final class TicketShowCommentRelationsFilesTest extends TestCase
{
public function test_ticket_show_only_eager_loads_real_comment_relations(): void
{
$source = file_get_contents(dirname(__DIR__, 2) . '/Livewire/TicketShow.php');
self::assertIsString($source);
self::assertStringContainsString("->with(['user.company', 'attachments'])", $source);
self::assertStringNotContainsString("->with(['user.company', 'owner:id,name,email,company_id', 'agent:id,name,email', 'attachments'])", $source);
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use PHPUnit\Framework\TestCase;
final class TicketUserNPlusOneFilesTest extends TestCase
{
public function test_ticket_queries_only_eager_load_required_relations(): void
{
$index = file_get_contents(dirname(__DIR__, 2).'/Livewire/TicketIndex.php');
$kanban = file_get_contents(dirname(__DIR__, 2).'/Livewire/KanbanBoard.php');
$dashboard = file_get_contents(dirname(__DIR__, 2).'/Livewire/HelpdeskDashboardWidget.php');
self::assertIsString($index);
self::assertIsString($kanban);
self::assertIsString($dashboard);
// Index deliberately avoids loading user relations.
self::assertStringNotContainsString("'user:id,name,email'", $index);
self::assertStringNotContainsString("'owner:id,name,email'", $index);
self::assertStringNotContainsString("'agent:id,name,email'", $index);
self::assertStringContainsString("'owner:id,name,email'", $kanban);
self::assertStringContainsString("'agent:id,name,email'", $kanban);
// Dashboard intentionally only loads the relations it renders.
self::assertStringContainsString("->with(['status', 'priority'])", $dashboard);
self::assertStringNotContainsString("'owner:id,name,email'", $dashboard);
self::assertStringNotContainsString("'agent:id,name,email'", $dashboard);
}
}