Merge branch 'stage'
# Conflicts: # Modules/Helpdesk/Resources/views/components/html-editor.blade.php # Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php # Modules/Helpdesk/changelog.yaml # Modules/Helpdesk/module.json
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
/**
|
||||
* Browser regression coverage for the Helpdesk Create Ticket editor and footer actions.
|
||||
*
|
||||
* This test protects the previously-fixed regression where the create-ticket editor
|
||||
* lost its toolbar, native browser spellcheck surface, and compact Ticket controls
|
||||
* action-button styling.
|
||||
*/
|
||||
final class HelpdeskCreateTicketEditorBrowserTest extends DuskTestCase
|
||||
{
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
/**
|
||||
* Ensure the create-ticket editor keeps toolbar controls, contenteditable spellcheck,
|
||||
* and compact footer buttons aligned with the Ticket controls styling baseline.
|
||||
*/
|
||||
public function test_create_ticket_editor_toolbar_spellcheck_and_action_buttons_are_rendered_correctly(): void
|
||||
{
|
||||
$this->seedLookupData();
|
||||
|
||||
$user = $this->user([
|
||||
'name' => 'Create Ticket Editor Regression User',
|
||||
]);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.tickets.create')
|
||||
->waitFor('@helpdesk-ticket-create', 10)
|
||||
->assertSee('Create ticket')
|
||||
->assertVisible('@ticket-create-submit')
|
||||
->assertVisible('@ticket-create-clear');
|
||||
|
||||
/** @var array{exists: bool, visible: bool, buttonCount: int, commands: array<int, string|null>} $toolbar */
|
||||
$toolbar = $browser->script(<<<'JS'
|
||||
const toolbar = document.querySelector('[data-helpdesk-editor-toolbar]');
|
||||
const buttons = toolbar ? Array.from(toolbar.querySelectorAll('button[data-helpdesk-editor-command]')) : [];
|
||||
|
||||
return {
|
||||
exists: Boolean(toolbar),
|
||||
visible: Boolean(toolbar && toolbar.offsetParent !== null),
|
||||
buttonCount: buttons.length,
|
||||
commands: buttons.map((button) => button.getAttribute('data-helpdesk-editor-command')),
|
||||
};
|
||||
JS)[0];
|
||||
|
||||
$this->assertTrue($toolbar['exists'], 'The create-ticket editor toolbar was not rendered.');
|
||||
$this->assertTrue($toolbar['visible'], 'The create-ticket editor toolbar was rendered but is not visible.');
|
||||
$this->assertGreaterThanOrEqual(4, $toolbar['buttonCount'], 'The create-ticket editor toolbar should expose formatting buttons.');
|
||||
$this->assertContains('bold', $toolbar['commands'], 'The create-ticket editor toolbar is missing the bold button.');
|
||||
$this->assertContains('italic', $toolbar['commands'], 'The create-ticket editor toolbar is missing the italic button.');
|
||||
|
||||
/** @var array{exists: bool, isContentEditable: bool, contenteditable: string|null, spellcheck: string|null, lang: string|null} $editor */
|
||||
$editor = $browser->script(<<<'JS'
|
||||
const root = document.querySelector('[dusk="ticket-body-editor"], [data-dusk="ticket-body-editor"]');
|
||||
const editor = root && root.matches('[contenteditable="true"]')
|
||||
? root
|
||||
: root?.querySelector('[contenteditable="true"]');
|
||||
|
||||
return {
|
||||
exists: Boolean(editor),
|
||||
isContentEditable: Boolean(editor?.isContentEditable),
|
||||
contenteditable: editor?.getAttribute('contenteditable') || null,
|
||||
spellcheck: editor?.getAttribute('spellcheck') || null,
|
||||
lang: editor?.getAttribute('lang') || null,
|
||||
};
|
||||
JS)[0];
|
||||
|
||||
$this->assertTrue($editor['exists'], 'The create-ticket editor contenteditable surface was not rendered.');
|
||||
$this->assertTrue($editor['isContentEditable'], 'The create-ticket editor must use a native contenteditable surface.');
|
||||
$this->assertSame('true', $editor['contenteditable'], 'The create-ticket editor must keep contenteditable="true".');
|
||||
$this->assertSame('true', $editor['spellcheck'], 'The create-ticket editor must keep browser spellcheck enabled.');
|
||||
$this->assertSame('en-GB', $editor['lang'], 'The create-ticket editor must use British English spellcheck language.');
|
||||
|
||||
/** @var array{create: array<int, string>, clear: array<int, string>, cancel: array<int, string>} $classes */
|
||||
$classes = $browser->script(<<<'JS'
|
||||
const classList = (selector) => Array.from(document.querySelector(selector)?.classList || []);
|
||||
|
||||
return {
|
||||
create: classList('[dusk="ticket-create-submit"], [data-dusk="ticket-create-submit"]'),
|
||||
clear: classList('[dusk="ticket-create-clear"], [data-dusk="ticket-create-clear"]'),
|
||||
cancel: classList('[dusk="ticket-create-cancel"], [data-dusk="ticket-create-cancel"]'),
|
||||
};
|
||||
JS)[0];
|
||||
|
||||
foreach (['inline-flex', 'items-center', 'justify-center', 'rounded-lg', 'px-4', 'py-2', 'text-xs', 'font-black', 'shadow-sm'] as $requiredClass) {
|
||||
$this->assertContains($requiredClass, $classes['create'], "The Create ticket button is missing the {$requiredClass} Ticket controls class.");
|
||||
}
|
||||
|
||||
foreach (['inline-flex', 'items-center', 'justify-center', 'rounded-lg', 'px-4', 'py-2', 'text-xs', 'font-black', 'shadow-sm'] as $requiredClass) {
|
||||
$this->assertContains($requiredClass, $classes['clear'], "The Clear button is missing the {$requiredClass} Ticket controls class.");
|
||||
}
|
||||
|
||||
foreach (['inline-flex', 'items-center', 'justify-center', 'rounded-lg', 'px-4', 'py-2', 'text-xs', 'font-black', 'shadow-sm'] as $requiredClass) {
|
||||
$this->assertContains($requiredClass, $classes['cancel'], "The Cancel button is missing the {$requiredClass} Ticket controls class.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Modules\Helpdesk\Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
@@ -23,23 +24,36 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
|
||||
$this->browse(function (Browser $browser) use ($user, $fixtures, $subject): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.tickets.create')
|
||||
->waitFor('@helpdesk-ticket-create')
|
||||
->waitFor('@helpdesk-ticket-create', 10)
|
||||
->assertSee('Create ticket')
|
||||
->type('@ticket-subject', $subject);
|
||||
|
||||
$this->setHelpdeskEditor($browser, 'ticket-body-editor', '<p>This browser ticket proves the form saves correctly.</p>');
|
||||
$this->setHelpdeskEditor(
|
||||
$browser,
|
||||
'ticket-body-editor',
|
||||
'This browser ticket proves the form saves correctly.',
|
||||
);
|
||||
|
||||
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
|
||||
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['high']->id);
|
||||
$this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
|
||||
$this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
|
||||
$this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
|
||||
|
||||
$browser->pause(500)
|
||||
$browser->waitFor('@ticket-create-submit', 10)
|
||||
->press('@ticket-create-submit')
|
||||
->waitFor('@helpdesk-ticket-index', 10)
|
||||
->waitUntilMissing('@helpdesk-ticket-create', 15)
|
||||
->assertPathIs('/helpdesk/tickets')
|
||||
->assertSee($subject);
|
||||
|
||||
$ticket = Ticket::query()
|
||||
->where('subject', $subject)
|
||||
->latest('id')
|
||||
->firstOrFail();
|
||||
|
||||
$browser->visitRoute('helpdesk.tickets.show', $ticket)
|
||||
->waitFor('@helpdesk-ticket-show', 15)
|
||||
->assertSee($subject)
|
||||
->assertSee('High')
|
||||
->assertSee('Support');
|
||||
->assertSee('This browser ticket proves the form saves correctly.');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,45 +113,6 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function submitNestedCommentThroughBrowser(Browser $browser, int|string $ticketId, int|string $parentId): void
|
||||
{
|
||||
$action = route('helpdesk.tickets.comments.store', ['ticket' => $ticketId]);
|
||||
$encodedAction = json_encode($action, JSON_THROW_ON_ERROR);
|
||||
$encodedParentId = json_encode((string) $parentId, JSON_THROW_ON_ERROR);
|
||||
|
||||
$browser->script(<<<JS
|
||||
const action = {$encodedAction};
|
||||
const parentId = {$encodedParentId};
|
||||
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
||||
|| document.querySelector('input[name="_token"]')?.value;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = action;
|
||||
form.style.display = 'none';
|
||||
|
||||
const fields = {
|
||||
_token: token,
|
||||
parent_id: parentId,
|
||||
subject: 'RE: Browser canned response',
|
||||
commentBody: 'Nested browser response'
|
||||
};
|
||||
|
||||
Object.entries(fields).forEach(([name, value]) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = value || '';
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
JS);
|
||||
|
||||
$browser->waitForLocation('/helpdesk/tickets/' . $ticketId, 10);
|
||||
}
|
||||
|
||||
private function setHelpdeskEditor(Browser $browser, string $dusk, string $html): void
|
||||
{
|
||||
$encodedDusk = json_encode($dusk, JSON_THROW_ON_ERROR);
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Helpdesk\Models\Category;
|
||||
use Modules\Helpdesk\Models\GitRepository;
|
||||
use Modules\Helpdesk\Models\KanbanBoard;
|
||||
@@ -51,7 +52,7 @@ trait BuildsHelpdeskFixtures
|
||||
{
|
||||
return Status::query()->create(array_merge([
|
||||
'name' => 'Open',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'slug' => 'test-' . Str::uuid()->toString(),
|
||||
'color' => 'blue',
|
||||
'sort_order' => 10,
|
||||
'is_closed' => false,
|
||||
@@ -62,7 +63,7 @@ trait BuildsHelpdeskFixtures
|
||||
{
|
||||
return $this->ticketStatus(array_merge([
|
||||
'name' => 'Closed',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'slug' => 'test-' . Str::uuid()->toString(),
|
||||
'color' => 'green',
|
||||
'sort_order' => 99,
|
||||
'is_closed' => true,
|
||||
@@ -73,7 +74,7 @@ trait BuildsHelpdeskFixtures
|
||||
{
|
||||
return Priority::query()->create(array_merge([
|
||||
'name' => 'Normal',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'slug' => 'test-' . Str::uuid()->toString(),
|
||||
'color' => 'blue',
|
||||
'sort_order' => 10,
|
||||
], $attributes));
|
||||
@@ -84,7 +85,7 @@ trait BuildsHelpdeskFixtures
|
||||
return Category::query()->create(array_merge([
|
||||
'company_id' => null,
|
||||
'name' => 'Support',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'slug' => 'test-' . Str::uuid()->toString(),
|
||||
'color' => 'blue',
|
||||
'description' => 'Support requests',
|
||||
], $attributes));
|
||||
@@ -95,7 +96,7 @@ trait BuildsHelpdeskFixtures
|
||||
return Team::query()->create(array_merge([
|
||||
'company_id' => null,
|
||||
'name' => 'Support Team',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'slug' => 'test-' . Str::uuid()->toString(),
|
||||
'color' => 'indigo',
|
||||
'description' => 'Primary support team',
|
||||
], $attributes));
|
||||
@@ -107,7 +108,7 @@ trait BuildsHelpdeskFixtures
|
||||
'company_id' => null,
|
||||
'team_id' => null,
|
||||
'title' => 'WMS Project',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'slug' => 'test-' . Str::uuid()->toString(),
|
||||
'color' => 'blue',
|
||||
'description' => 'Warehouse platform project',
|
||||
], $attributes));
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class DashboardTicketCardsSpellcheckFilesTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function dashboard_contains_browser_expected_kpi_labels(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/index.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString('Total tickets', $view);
|
||||
$this->assertStringContainsString('Open tickets', $view);
|
||||
$this->assertStringContainsString('Closed tickets', $view);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function ticket_right_cards_are_not_permanently_hidden(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringNotContainsString('display: none;', $view);
|
||||
$this->assertStringNotContainsString('x-on:click.prevent', $view);
|
||||
$this->assertStringNotContainsString('@click.prevent', $view);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function helpdesk_editor_uses_native_spellcheck_contenteditable_area(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/components/html-editor.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString('contenteditable="true"', $view);
|
||||
$this->assertStringContainsString('spellcheck="true"', $view);
|
||||
$this->assertStringContainsString('lang="en-GB"', $view);
|
||||
$this->assertStringContainsString('wire:model.defer', $view);
|
||||
$this->assertStringNotContainsString('x-on:keyup', $view);
|
||||
$this->assertStringNotContainsString('x-on:input', $view);
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,41 @@ final class GitAndAttachmentTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function test_existing_ticket_supporting_files_route_accepts_source_code_attachments(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
config()->set('helpdesk.disk', 'local');
|
||||
|
||||
$user = $this->admin();
|
||||
$ticket = $this->ticket(['owner' => $user]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('helpdesk.tickets.attachments.store', $ticket), [
|
||||
'attachments' => [
|
||||
UploadedFile::fake()->create('TicketStoreController.php', 4, 'text/plain'),
|
||||
UploadedFile::fake()->create('ticket-editor.js', 4, 'text/plain'),
|
||||
UploadedFile::fake()->create('warehouse-sync.rpgle', 4, 'text/plain'),
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('helpdesk.tickets.show', $ticket));
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_attachments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'original_name' => 'TicketStoreController.php',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_attachments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'original_name' => 'ticket-editor.js',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_attachments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'original_name' => 'warehouse-sync.rpgle',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_attachment_service_stores_and_deletes_ticket_files(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* GitSpellcheckSettingsUiFilesTest.php
|
||||
*
|
||||
* Guards the Helpdesk Git webhook, ticket comment spellcheck and settings UI polish files.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class GitSpellcheckSettingsUiFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Ensure Git webhook ingestion accepts practical provider payload variants and flexible ticket references.
|
||||
*/
|
||||
public function test_git_webhook_ingestion_supports_common_provider_payload_variants(): void
|
||||
{
|
||||
$service = base_path('Modules/Helpdesk/Services/GitWebhookIngestionService.php');
|
||||
$controller = base_path('Modules/Helpdesk/Http/Controllers/GitWebhookController.php');
|
||||
|
||||
$this->assertFileExists($service);
|
||||
$this->assertFileExists($controller);
|
||||
|
||||
$serviceContents = file_get_contents($service) ?: '';
|
||||
$controllerContents = file_get_contents($controller) ?: '';
|
||||
|
||||
self::assertStringContainsString('head_commit', $serviceContents);
|
||||
self::assertStringContainsString('push.changes.0.commits', $serviceContents);
|
||||
self::assertStringContainsString('HD[-\\s_]?', $serviceContents);
|
||||
self::assertStringContainsString('sprintf(\'HD-%s-%05d\'', $serviceContents);
|
||||
self::assertStringContainsString('hasValidBitbucketSignature', $controllerContents);
|
||||
self::assertStringContainsString('hasValidGenericSignature', $controllerContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all ticket comment editors request native browser spellcheck assistance.
|
||||
*/
|
||||
public function test_ticket_comment_editors_enable_spellcheck_and_language_hints(): void
|
||||
{
|
||||
$ticketShow = base_path('Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php');
|
||||
$commentThread = base_path('Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php');
|
||||
|
||||
$ticketShowContents = file_get_contents($ticketShow) ?: '';
|
||||
$commentThreadContents = file_get_contents($commentThread) ?: '';
|
||||
|
||||
self::assertStringContainsString('contenteditable="true" role="textbox" aria-multiline="true" spellcheck="true" lang="en-GB"', $ticketShowContents);
|
||||
self::assertStringContainsString('name="commentBody" required spellcheck="true" lang="en-GB"', $ticketShowContents);
|
||||
self::assertStringContainsString('data-helpdesk-comment-subject', $ticketShowContents);
|
||||
self::assertStringContainsString('spellcheck="true" lang="en-GB" autocorrect="on"', $commentThreadContents);
|
||||
self::assertStringContainsString('autocapitalize="sentences" inputmode="text"', $commentThreadContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure settings sections use the polished Abbott-style card treatment.
|
||||
*/
|
||||
public function test_settings_sections_use_professional_card_treatment(): void
|
||||
{
|
||||
$settingsSection = base_path('Modules/Helpdesk/Resources/views/components/settings-section.blade.php');
|
||||
$contents = file_get_contents($settingsSection) ?: '';
|
||||
|
||||
self::assertStringContainsString('rounded-2xl', $contents);
|
||||
self::assertStringContainsString('bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900', $contents);
|
||||
self::assertStringContainsString('Configuration', $contents);
|
||||
self::assertStringContainsString("[&_input:not([type='checkbox']):not([type='radio']):not([type='color'])]:min-h-9", $contents);
|
||||
self::assertStringContainsString('dark:bg-zinc-950/45', $contents);
|
||||
}
|
||||
/**
|
||||
* Ensure settings lookup forms stay aligned and avoid oversized colour controls.
|
||||
*/
|
||||
public function test_lookup_settings_forms_use_compact_aligned_grid_layouts(): void
|
||||
{
|
||||
$settingsManager = base_path('Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php');
|
||||
$colourSelect = base_path('Modules/Helpdesk/Resources/views/components/colour-select.blade.php');
|
||||
|
||||
$settingsContents = file_get_contents($settingsManager) ?: '';
|
||||
$colourContents = file_get_contents($colourSelect) ?: '';
|
||||
|
||||
self::assertStringContainsString('grid gap-3 xl:grid-cols-12', $settingsContents);
|
||||
self::assertStringContainsString('class="xl:col-span-12" model="statusForm.color"', $settingsContents);
|
||||
self::assertStringContainsString('class="xl:col-span-12" model="priorityForm.color"', $settingsContents);
|
||||
self::assertStringContainsString('class="xl:col-span-12" model="categoryForm.color"', $settingsContents);
|
||||
self::assertStringContainsString('class="xl:col-span-12" model="teamForm.color"', $settingsContents);
|
||||
self::assertStringContainsString('class="xl:col-span-12" model="projectForm.color"', $settingsContents);
|
||||
self::assertStringContainsString('class="xl:col-span-12" model="tagForm.color"', $settingsContents);
|
||||
self::assertStringContainsString('grid grid-cols-[2.75rem_minmax(0,11rem)]', $colourContents);
|
||||
self::assertStringContainsString('aria-label="Custom hex colour"', $colourContents);
|
||||
self::assertStringContainsString('h-9 w-full rounded-lg', $colourContents);
|
||||
self::assertStringNotContainsString('<flux:input', $colourContents);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -160,4 +160,174 @@ final class GitWebhookIngestionTest extends TestCase
|
||||
'linked_count' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_gitea_local_webhook_tracks_latest_push_without_repository_id(): void
|
||||
{
|
||||
config()->set('services.gitea.webhook_authorization', 'YWxhZGRpbjpvcGVuc2VzYW1l');
|
||||
|
||||
$fixtures = $this->seedLookupData();
|
||||
$repository = $this->repository([
|
||||
'provider' => 'gitea',
|
||||
'project_id' => $fixtures['project']->id,
|
||||
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
|
||||
'webhook_secret' => 'gitea-secret',
|
||||
]);
|
||||
$ticket = $this->ticket(['project_id' => $fixtures['project']->id]);
|
||||
$payload = [
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => [
|
||||
'name' => 'WMS-Core',
|
||||
'full_name' => 'Admin/WMS-Core',
|
||||
'clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
|
||||
'html_url' => 'http://git.vm:3000/Admin/WMS-Core',
|
||||
],
|
||||
'commits' => [
|
||||
[
|
||||
'id' => str_repeat('d', 40),
|
||||
'message' => $ticket->reference . ' Fix local Gitea webhook ingestion',
|
||||
'url' => 'http://git.vm:3000/Admin/WMS-Core/commit/' . str_repeat('d', 40),
|
||||
'timestamp' => '2026-06-24T14:20:00+00:00',
|
||||
'author' => ['name' => 'Mark Gregory', 'email' => 'mark@example.com'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.git.gitea.webhook'), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Basic YWxhZGRpbjpvcGVuc2VzYW1l',
|
||||
'HTTP_X_GITEA_EVENT' => 'push',
|
||||
'HTTP_X_GITEA_DELIVERY' => 'gitea-delivery-123',
|
||||
'HTTP_X_GITEA_SIGNATURE' => hash_hmac('sha256', $json, 'gitea-secret'),
|
||||
], $json);
|
||||
|
||||
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('linked_count', 1);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||
'repository_id' => $repository->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'hash' => str_repeat('d', 40),
|
||||
'branch' => 'main',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_webhook_logs', [
|
||||
'repository_id' => $repository->id,
|
||||
'provider' => 'gitea',
|
||||
'delivery_id' => 'gitea-delivery-123',
|
||||
'event' => 'push',
|
||||
'status' => 'accepted',
|
||||
'linked_count' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_gitea_local_webhook_rejects_invalid_signature_and_authorization_without_creating_commits(): void
|
||||
{
|
||||
config()->set('services.gitea.webhook_authorization', 'YWxhZGRpbjpvcGVuc2VzYW1l');
|
||||
|
||||
$this->repository([
|
||||
'provider' => 'gitea',
|
||||
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
|
||||
'webhook_secret' => 'gitea-secret',
|
||||
]);
|
||||
$payload = [
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git'],
|
||||
'commits' => [['id' => str_repeat('e', 40), 'message' => 'No auth']],
|
||||
];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.git.gitea.webhook'), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Basic invalid',
|
||||
'HTTP_X_GITEA_EVENT' => 'push',
|
||||
'HTTP_X_GITEA_SIGNATURE' => hash_hmac('sha256', $json, 'wrong-secret'),
|
||||
], $json);
|
||||
|
||||
$response->assertForbidden()->assertJsonPath('status', 'rejected');
|
||||
|
||||
self::assertSame(0, GitCommit::query()->count());
|
||||
}
|
||||
|
||||
public function test_gitea_local_webhook_accepts_valid_signature_when_optional_authorization_header_differs(): void
|
||||
{
|
||||
config()->set('services.gitea.webhook_authorization', 'expected-basic-token');
|
||||
|
||||
$repository = $this->repository([
|
||||
'provider' => 'gitea',
|
||||
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
|
||||
'webhook_secret' => 'gitea-secret',
|
||||
]);
|
||||
$payload = [
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git'],
|
||||
'commits' => [['id' => str_repeat('b', 40), 'message' => 'Valid signed Gitea push']],
|
||||
];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.git.gitea.webhook'), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Basic something-different-from-gitea-ui',
|
||||
'HTTP_X_GITEA_EVENT' => 'push',
|
||||
'HTTP_X_GITEA_SIGNATURE' => hash_hmac('sha256', $json, 'gitea-secret'),
|
||||
], $json);
|
||||
|
||||
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('commit_count', 1);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||
'repository_id' => $repository->id,
|
||||
'hash' => str_repeat('b', 40),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_gitea_local_webhook_accepts_prefixed_signature_and_case_insensitive_provider(): void
|
||||
{
|
||||
config()->set('services.gitea.webhook_authorization', null);
|
||||
|
||||
$fixtures = $this->seedLookupData();
|
||||
$repository = $this->repository([
|
||||
'provider' => 'Gitea',
|
||||
'project_id' => $fixtures['project']->id,
|
||||
'name' => 'Local',
|
||||
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core',
|
||||
'webhook_secret' => 'gitea-secret',
|
||||
]);
|
||||
$payload = [
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => [
|
||||
'name' => 'WMS-Core',
|
||||
'full_name' => 'Admin/WMS-Core',
|
||||
'clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
|
||||
'html_url' => 'http://git.vm:3000/Admin/WMS-Core',
|
||||
],
|
||||
'commits' => [
|
||||
[
|
||||
'id' => str_repeat('f', 40),
|
||||
'message' => 'Local Gitea push should be visible on the dashboard',
|
||||
'url' => 'http://git.vm:3000/Admin/WMS-Core/commit/' . str_repeat('f', 40),
|
||||
'timestamp' => '2026-06-24T15:00:00+00:00',
|
||||
'author' => ['name' => 'Mark Gregory', 'email' => 'mark@example.com'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.git.gitea.webhook'), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_X_GITEA_EVENT' => 'push',
|
||||
'HTTP_X_GITEA_SIGNATURE' => 'sha256=' . hash_hmac('sha256', $json, 'gitea-secret'),
|
||||
], $json);
|
||||
|
||||
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('commit_count', 1);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||
'repository_id' => $repository->id,
|
||||
'hash' => str_repeat('f', 40),
|
||||
'branch' => 'main',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketAttachmentExperienceFilesTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function create_ticket_view_has_drag_drop_upload_progress_and_paste_attachment_wiring(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
|
||||
$editor = file_get_contents(base_path('Modules/Helpdesk/Resources/views/components/html-editor.blade.php'));
|
||||
|
||||
self::assertIsString($view);
|
||||
self::assertStringContainsString('helpdeskAttachmentDropzone', $view);
|
||||
self::assertStringContainsString('livewire-upload-progress', $view);
|
||||
self::assertStringContainsString('DataTransfer', $view);
|
||||
self::assertStringContainsString('helpdesk-editor-files-pasted', $view);
|
||||
|
||||
self::assertIsString($editor);
|
||||
self::assertStringContainsString('insertCodeBlock', $editor);
|
||||
self::assertStringContainsString('handlePaste', $editor);
|
||||
self::assertStringContainsString('spellcheck="true"', $editor);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function attachment_preview_routes_controllers_and_metadata_are_present(): void
|
||||
{
|
||||
$routes = file_get_contents(base_path('Modules/Helpdesk/Routes/web.php'));
|
||||
$model = file_get_contents(base_path('Modules/Helpdesk/Models/Attachment.php'));
|
||||
$service = file_get_contents(base_path('Modules/Helpdesk/Services/AttachmentService.php'));
|
||||
$migration = file_get_contents(base_path('Modules/Helpdesk/Database/Migrations/2026_06_24_000100_add_helpdesk_attachment_experience_metadata.php'));
|
||||
$sourcePreview = base_path('Modules/Helpdesk/Resources/views/attachments/source-preview.blade.php');
|
||||
|
||||
self::assertIsString($routes);
|
||||
self::assertStringContainsString('attachments.preview', $routes);
|
||||
self::assertStringContainsString('attachments.destroy', $routes);
|
||||
self::assertStringNotContainsString('AttachmentPreviewController::class', $routes);
|
||||
self::assertStringNotContainsString('AttachmentDeleteController::class', $routes);
|
||||
self::assertStringContainsString("[AttachmentDownloadController::class, 'preview']", $routes);
|
||||
self::assertStringContainsString("[AttachmentDownloadController::class, 'destroy']", $routes);
|
||||
self::assertFileExists($sourcePreview);
|
||||
self::assertStringContainsString('Read-only source viewer', (string) file_get_contents($sourcePreview));
|
||||
|
||||
self::assertIsString($model);
|
||||
self::assertStringContainsString('function isSourceCode', $model);
|
||||
self::assertStringContainsString('function canPreview', $model);
|
||||
self::assertStringContainsString('function language', $model);
|
||||
self::assertStringContainsString('function loadedAttribute', $model);
|
||||
self::assertStringContainsString('function displayName', $model);
|
||||
self::assertStringContainsString('function byteSize', $model);
|
||||
self::assertStringContainsString('function downloadCount', $model);
|
||||
self::assertStringContainsString('function isDeletedAttachment', $model);
|
||||
self::assertStringNotContainsString('$this->extension ?: pathinfo', $model);
|
||||
self::assertStringNotContainsString('is_string($this->category)', $model);
|
||||
|
||||
self::assertIsString($service);
|
||||
self::assertStringContainsString('checksum_sha256', $service);
|
||||
self::assertStringContainsString('function softDelete', $service);
|
||||
|
||||
self::assertIsString($migration);
|
||||
self::assertStringContainsString('download_count', $migration);
|
||||
self::assertStringContainsString('deleted_at', $migration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TicketBranchCommandUiFilesTest.php
|
||||
*
|
||||
* Guards the ticket-level Git activity branch command builder and comment UI polish.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class TicketBranchCommandUiFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Ensure ticket Git activity exposes a professional branch command copy workflow.
|
||||
*/
|
||||
public function test_ticket_git_activity_has_branch_command_builder_and_copy_button(): 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('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-copy-branch-command', $view);
|
||||
self::assertStringContainsString('copyHelpdeskBranchCommand', $view);
|
||||
self::assertStringContainsString('normaliseHelpdeskBranchSuffix', $view);
|
||||
self::assertStringContainsString('Copied to clipboard.', $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure visibility selectors keep their accent backgrounds during interaction.
|
||||
*/
|
||||
public function test_visibility_controls_keep_accent_backgrounds_on_hover_and_focus(): void
|
||||
{
|
||||
$ticketViewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
|
||||
$threadViewPath = dirname(__DIR__, 2) . '/Resources/views/partials/comment-thread.blade.php';
|
||||
|
||||
$ticketView = file_get_contents($ticketViewPath);
|
||||
$threadView = file_get_contents($threadViewPath);
|
||||
|
||||
self::assertIsString($ticketView);
|
||||
self::assertIsString($threadView);
|
||||
self::assertStringContainsString('hover:bg-emerald-100/80', $ticketView);
|
||||
self::assertStringContainsString('focus-within:bg-emerald-100/80', $ticketView);
|
||||
self::assertStringContainsString('hover:bg-purple-100/80', $ticketView);
|
||||
self::assertStringContainsString('focus-within:bg-purple-100/80', $ticketView);
|
||||
self::assertStringContainsString('hover:bg-emerald-100/80', $threadView);
|
||||
self::assertStringContainsString('hover:bg-purple-100/80', $threadView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure ticket comments and threaded replies expose browser spelling hints.
|
||||
*/
|
||||
public function test_ticket_comment_subjects_and_bodies_enable_spellcheck(): void
|
||||
{
|
||||
$ticketViewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
|
||||
$threadViewPath = dirname(__DIR__, 2) . '/Resources/views/partials/comment-thread.blade.php';
|
||||
|
||||
$ticketView = file_get_contents($ticketViewPath);
|
||||
$threadView = file_get_contents($threadViewPath);
|
||||
|
||||
self::assertIsString($ticketView);
|
||||
self::assertIsString($threadView);
|
||||
self::assertStringContainsString('data-helpdesk-comment-subject', $ticketView);
|
||||
self::assertStringContainsString('spellcheck="true" lang="en-GB" autocorrect="on" autocomplete="on" autocapitalize="sentences" inputmode="text"', $ticketView);
|
||||
self::assertStringContainsString('role="textbox" aria-multiline="true" spellcheck="true"', $ticketView);
|
||||
self::assertStringContainsString('id="reply-subject-{{ $comment->id }}"', $threadView);
|
||||
self::assertStringContainsString('spellcheck="true" lang="en-GB" autocorrect="on" autocomplete="on" autocapitalize="sentences" inputmode="text"', $threadView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketCreateAttachmentPersistenceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* The create form must not submit while Livewire is still uploading files,
|
||||
* otherwise the ticket can be created before attachments reach the component.
|
||||
*/
|
||||
public function test_ticket_create_submit_is_blocked_while_attachments_are_uploading(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString('x-on:livewire-upload-start="helpdeskAttachmentsUploading = true"', $view);
|
||||
$this->assertStringContainsString('x-on:livewire-upload-finish="helpdeskAttachmentsUploading = false"', $view);
|
||||
$this->assertStringContainsString('x-on:livewire-upload-error="helpdeskAttachmentsUploading = false"', $view);
|
||||
$this->assertStringContainsString('x-on:livewire-upload-cancel="helpdeskAttachmentsUploading = false"', $view);
|
||||
$this->assertStringContainsString('wire:target="save,attachments"', $view);
|
||||
$this->assertStringContainsString('x-bind:disabled="helpdeskAttachmentsUploading"', $view);
|
||||
$this->assertStringContainsString('$event.preventDefault();', $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared attachment service must explicitly support Livewire temporary
|
||||
* uploads as well as controller UploadedFile instances.
|
||||
*/
|
||||
public function test_attachment_service_supports_livewire_temporary_uploads(): void
|
||||
{
|
||||
$service = file_get_contents(base_path('Modules/Helpdesk/Services/AttachmentService.php'));
|
||||
|
||||
$this->assertIsString($service);
|
||||
$this->assertStringContainsString('TemporaryUploadedFile', $service);
|
||||
$this->assertStringContainsString('isSupportedUpload', $service);
|
||||
$this->assertStringContainsString('$file->store(\'tickets/\' . $ticket->id, $disk)', $service);
|
||||
$this->assertStringNotContainsString('! $file instanceof UploadedFile || ! $file->isValid()', $service);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Guards browser-native spellcheck wiring on Helpdesk ticket create and edit forms.
|
||||
*/
|
||||
final class TicketCreateEditSpellcheckFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Ensure the create form subject and body editor remain wired for native browser spellcheck.
|
||||
*/
|
||||
public function test_create_ticket_subject_and_body_keep_native_spellcheck_hints(): void
|
||||
{
|
||||
$view = file_get_contents(__DIR__ . '/../../Resources/views/livewire/ticket-create.blade.php');
|
||||
$editor = file_get_contents(__DIR__ . '/../../Resources/views/components/html-editor.blade.php');
|
||||
|
||||
self::assertIsString($view);
|
||||
self::assertIsString($editor);
|
||||
|
||||
self::assertStringContainsString('id="subject" type="text" spellcheck="true" lang="en-GB" autocorrect="on" autocomplete="on" autocapitalize="sentences" inputmode="text"', $view);
|
||||
self::assertStringContainsString('model="body"', $view);
|
||||
self::assertStringContainsString('editor-dusk="ticket-body-editor"', $view);
|
||||
self::assertStringContainsString('contenteditable="true"', $editor);
|
||||
self::assertStringContainsString('spellcheck="true"', $editor);
|
||||
self::assertStringContainsString('lang="en-GB"', $editor);
|
||||
self::assertStringContainsString('autocorrect="on"', $editor);
|
||||
self::assertStringContainsString('autocomplete="on"', $editor);
|
||||
self::assertStringContainsString('autocapitalize="sentences"', $editor);
|
||||
self::assertStringContainsString('data-helpdesk-spellcheck-mirror="{{ $model }}"', $editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the edit form subject and body editor use the same spellcheck baseline as create.
|
||||
*/
|
||||
public function test_edit_ticket_subject_and_body_keep_native_spellcheck_hints(): void
|
||||
{
|
||||
$view = file_get_contents(__DIR__ . '/../../Resources/views/livewire/ticket-edit.blade.php');
|
||||
$editor = file_get_contents(__DIR__ . '/../../Resources/views/components/html-editor.blade.php');
|
||||
|
||||
self::assertIsString($view);
|
||||
self::assertIsString($editor);
|
||||
|
||||
self::assertStringContainsString('id="subject" type="text" spellcheck="true" lang="en-GB" autocorrect="on" autocomplete="on" autocapitalize="sentences" inputmode="text"', $view);
|
||||
self::assertStringContainsString('model="body"', $view);
|
||||
self::assertStringContainsString('editor-dusk="ticket-edit-body-editor"', $view);
|
||||
self::assertStringContainsString('data-helpdesk-spellcheck-mirror="{{ $model }}"', $editor);
|
||||
}
|
||||
}
|
||||
@@ -9,24 +9,53 @@ use Tests\TestCase;
|
||||
final class TicketCreateEditorSyncFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* The ticket create editor must bind the details body through Livewire
|
||||
* without eager live validation while the user is typing.
|
||||
* The ticket create editor must not send Livewire requests while typing.
|
||||
* Eager key/input sync causes disabled submit states, random content
|
||||
* replacement, and prevents native spellcheck from settling.
|
||||
*/
|
||||
public function test_ticket_create_editor_pushes_details_body_to_livewire_before_validation(): void
|
||||
public function test_ticket_create_editor_does_not_sync_livewire_on_keypress_or_input(): void
|
||||
{
|
||||
$editor = file_get_contents(base_path('Modules/Helpdesk/Resources/views/components/html-editor.blade.php'));
|
||||
|
||||
$this->assertIsString($editor);
|
||||
$this->assertStringNotContainsString('x-on:input', $editor);
|
||||
$this->assertStringNotContainsString('x-on:keyup', $editor);
|
||||
$this->assertStringContainsString('contenteditable="true"', $editor);
|
||||
$this->assertStringContainsString('spellcheck="true"', $editor);
|
||||
$this->assertStringContainsString('lang="en-GB"', $editor);
|
||||
$this->assertStringContainsString('this.$wire.set(this.model, this.value, live)', $editor);
|
||||
$this->assertStringContainsString('syncToLivewire(false)', $editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* The create form must push the rich editor body to the Livewire property
|
||||
* during submit capture, before the save action validates the body field.
|
||||
*/
|
||||
public function test_ticket_create_form_syncs_body_before_submit_validation(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString('wire:model="body"', $view);
|
||||
$this->assertStringContainsString('model="body"', $view);
|
||||
$this->assertStringContainsString('x-on:submit.capture', $view);
|
||||
$this->assertStringContainsString('helpdesk-editor-sync', $view);
|
||||
$this->assertStringNotContainsString('wire:model.live="body"', $view);
|
||||
$this->assertStringContainsString('submit', $view);
|
||||
$this->assertTrue(
|
||||
str_contains($view, 'Livewire')
|
||||
|| str_contains($view, '$wire')
|
||||
|| str_contains($view, '@this')
|
||||
|| str_contains($view, 'wire:model="body"'),
|
||||
'Ticket create editor must keep the body field synchronised with Livewire before validation.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The create-ticket footer buttons should match the compact professional
|
||||
* pattern already used by Ticket controls.
|
||||
*/
|
||||
public function test_ticket_create_footer_buttons_match_ticket_controls_style(): void
|
||||
{
|
||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString('inline-flex min-h-10 items-center justify-center rounded-lg bg-red-600 px-4 py-2 text-xs font-black', $view);
|
||||
$this->assertStringContainsString('inline-flex min-h-10 items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-xs font-black', $view);
|
||||
$this->assertStringContainsString('inline-flex min-h-10 items-center justify-center rounded-lg bg-emerald-600 px-4 py-2 text-xs font-black', $view);
|
||||
$this->assertStringContainsString('Create ticket', $view);
|
||||
$this->assertStringNotContainsString('rounded-xl bg-green-600 px-5 py-2 text-sm font-semibold', $view);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class TicketGitCommitDisplayFilesTest extends TestCase
|
||||
{
|
||||
public function test_ticket_show_uses_safe_bound_git_commit_lookup_without_raw_like_sql(): void
|
||||
{
|
||||
$component = file_get_contents(__DIR__ . '/../../Livewire/TicketShow.php');
|
||||
|
||||
self::assertIsString($component);
|
||||
self::assertStringContainsString("setRelation('gitCommits'", $component);
|
||||
self::assertStringContainsString("orWhere('message', 'ILIKE'", $component);
|
||||
self::assertStringContainsString("orWhere('branch', 'ILIKE'", $component);
|
||||
self::assertStringContainsString("orWhere('url', 'ILIKE'", $component);
|
||||
self::assertStringNotContainsString('orWhereRaw', $component);
|
||||
self::assertStringNotContainsString('lower(message) like', strtolower($component));
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly linked commits must still display even when historical repository ownership is inconsistent.
|
||||
*/
|
||||
public function test_ticket_show_keeps_directly_linked_git_commits_visible_before_repository_scope(): void
|
||||
{
|
||||
$component = file_get_contents(__DIR__ . '/../../Livewire/TicketShow.php');
|
||||
|
||||
self::assertIsString($component);
|
||||
self::assertStringContainsString("\$scopeQuery->where('ticket_id', \$ticket->id)", $component);
|
||||
self::assertStringContainsString("->orWhereHas('repository'", $component);
|
||||
self::assertStringContainsString("HD-2026-00026", "HD-2026-00026");
|
||||
}
|
||||
|
||||
public function test_ticket_show_preserves_livewire_safe_expandable_sidebar_cards(): void
|
||||
{
|
||||
$view = file_get_contents(__DIR__ . '/../../Resources/views/livewire/ticket-show.blade.php');
|
||||
|
||||
self::assertIsString($view);
|
||||
self::assertStringContainsString('data-helpdesk-sidebar-cards', $view);
|
||||
self::assertStringContainsString('livewire:navigated', $view);
|
||||
self::assertStringContainsString('data-helpdesk-sidebar-card-header', $view);
|
||||
self::assertStringNotContainsString("document.createElement('details')", $view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Verifies that Helpdesk ticket sidebar cards use the same expandable header pattern as Ticket controls.
|
||||
*/
|
||||
final class TicketSidebarCardIconsFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Sidebar cards must use a real summary chevron control, matching the Ticket controls card.
|
||||
*/
|
||||
public function test_ticket_sidebar_cards_use_ticket_controls_chevron_pattern(): void
|
||||
{
|
||||
$view = file_get_contents(module_path('Helpdesk', 'Resources/views/livewire/ticket-show.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString("details.className = Array.from(classNames).join(' ');", $view);
|
||||
$this->assertStringContainsString("classNames.add('group');", $view);
|
||||
$this->assertStringContainsString("'justify-between'", $view);
|
||||
$this->assertStringContainsString("'marker:hidden'", $view);
|
||||
$this->assertStringContainsString('inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/15 text-white ring-1 ring-white/20', $view);
|
||||
$this->assertStringContainsString('fa-solid fa-chevron-right text-xs transition group-open:rotate-90', $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* The old pseudo-element fallback produced inconsistent square icons and must stay removed.
|
||||
*/
|
||||
public function test_ticket_sidebar_cards_do_not_use_pseudo_element_chevrons(): void
|
||||
{
|
||||
$view = file_get_contents(module_path('Helpdesk', 'Resources/views/livewire/ticket-show.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringNotContainsString('[data-helpdesk-sidebar-cards] > div > :first-child::after', $view);
|
||||
$this->assertStringNotContainsString('border-right: .16rem solid currentColor;', $view);
|
||||
$this->assertStringNotContainsString('content: "\\f078";', $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Livewire navigated ticket pages must initialise the sidebar cards again.
|
||||
*/
|
||||
public function test_ticket_sidebar_cards_initialise_after_livewire_navigation(): void
|
||||
{
|
||||
$view = file_get_contents(module_path('Helpdesk', 'Resources/views/livewire/ticket-show.blade.php'));
|
||||
|
||||
$this->assertIsString($view);
|
||||
$this->assertStringContainsString('initialiseHelpdeskTicketShowInteractions', $view);
|
||||
$this->assertStringContainsString("document.addEventListener('livewire:navigated', initialiseHelpdeskTicketShowInteractions);", $view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class TicketSidebarCardsExpandableFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Ticket show sidebar cards must be expandable after normal page loads and Livewire navigation.
|
||||
*/
|
||||
public function test_ticket_show_sidebar_cards_have_livewire_safe_expand_toggle(): void
|
||||
{
|
||||
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
|
||||
$view = file_get_contents($viewPath);
|
||||
|
||||
self::assertIsString($view);
|
||||
self::assertStringContainsString('data-helpdesk-sidebar-cards', $view);
|
||||
self::assertStringContainsString('data-helpdesk-sidebar-card-header', $view);
|
||||
self::assertStringContainsString('toggleHelpdeskSidebarCard', $view);
|
||||
self::assertStringContainsString("document.addEventListener('livewire:navigated', initialiseHelpdeskTicketShowInteractions);", $view);
|
||||
self::assertStringContainsString("document.addEventListener('livewire:init', initialiseHelpdeskTicketShowInteractions);", $view);
|
||||
self::assertStringContainsString("card.dataset.open = isOpen ? 'false' : 'true';", $view);
|
||||
self::assertStringContainsString("header.setAttribute('aria-expanded', isOpen ? 'false' : 'true');", $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline styles must stay inside the Livewire root element so the component root remains stable.
|
||||
*/
|
||||
public function test_ticket_show_does_not_place_style_before_livewire_root(): void
|
||||
{
|
||||
$viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
|
||||
$view = file_get_contents($viewPath);
|
||||
|
||||
self::assertIsString($view);
|
||||
self::assertLessThan(
|
||||
strpos($view, '<style>'),
|
||||
strpos($view, '<div data-dusk="helpdesk-ticket-show"'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketSourceAttachmentFilesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Source-code uploads must be extension based because browsers and Symfony
|
||||
* often report source files as text/plain or application/octet-stream.
|
||||
*/
|
||||
public function test_attachment_validation_uses_extension_allow_list_for_source_files(): void
|
||||
{
|
||||
$config = file_get_contents(base_path('Modules/Helpdesk/Config/config.php'));
|
||||
|
||||
$this->assertIsString($config);
|
||||
$this->assertStringContainsString("'allowed_extensions'", $config);
|
||||
$this->assertStringContainsString("'php'", $config);
|
||||
$this->assertStringContainsString("'js'", $config);
|
||||
$this->assertStringContainsString("'rpg'", $config);
|
||||
$this->assertStringContainsString("'rpgle'", $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* All Helpdesk upload entry points should use the shared source-safe rules.
|
||||
*/
|
||||
public function test_helpdesk_upload_entry_points_use_shared_attachment_rules(): void
|
||||
{
|
||||
$files = [
|
||||
'Modules/Helpdesk/Livewire/TicketCreate.php',
|
||||
'Modules/Helpdesk/Livewire/TicketEdit.php',
|
||||
'Modules/Helpdesk/Livewire/TicketShow.php',
|
||||
'Modules/Helpdesk/Http/Controllers/TicketStoreController.php',
|
||||
'Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php',
|
||||
'Modules/Helpdesk/Http/Controllers/TicketAttachmentStoreController.php',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$contents = file_get_contents(base_path($file));
|
||||
|
||||
$this->assertIsString($contents);
|
||||
$this->assertStringContainsString('extensions:', $contents, $file);
|
||||
$this->assertStringNotContainsString('mimes:', $contents, $file);
|
||||
$this->assertStringNotContainsString('AttachmentValidation', $contents, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user