fully working tests both phpunit and dusk

This commit is contained in:
Mark Gregory
2026-06-29 11:14:47 +01:00
parent eaa8c6ebe3
commit 8a07f14b63
7 changed files with 169 additions and 90 deletions
+3 -45
View File
@@ -86,54 +86,10 @@ final class TicketCreate extends Component
$this->applyDefaults();
}
public function save(TicketService $tickets)
{
logger()->info('SAVE START');
try {
logger()->info('VALIDATING');
$this->validate();
logger()->info('VALIDATED');
logger()->info([
'attachments' => count($this->attachments),
'body' => $this->body,
]);
$ticket = $tickets->create(Auth::user(), [
'company_id' => $this->company_id,
'subject' => $this->subject,
'body' => $this->body,
'board_id' => $this->board_id,
'priority_id' => $this->priority_id,
'category_id' => $this->category_id,
'project_id' => $this->project_id,
'product_id' => $this->product_id,
'team_id' => $this->team_id,
'due_at' => $this->due_at,
'tag_ids' => $this->tag_ids,
], $this->attachments);
session()->flash('success', 'Ticket ' . $ticket->reference . ' has been created.');
logger()->info('CREATED '.$ticket->id);
return redirect()->route('helpdesk.tickets.index');
} catch (\Throwable $e) {
logger()->error($e);
throw $e;
}
}
/**
* Store a new helpdesk ticket.
*/
public function save_old(TicketService $tickets)
public function save(TicketService $tickets)
{
if ($this->isSaving) {
return null;
@@ -159,7 +115,9 @@ final class TicketCreate extends Component
'due_at' => $this->due_at,
'tag_ids' => $this->tag_ids,
], $this->attachments);
session()->flash('success', 'Ticket ' . $ticket->reference . ' has been created.');
return redirect()->to(route('helpdesk.tickets.index'));
} finally {
$this->isSaving = false;
@@ -31,15 +31,13 @@
this.$refs.editor.textContent = nextValue;
},
syncToLivewire(live = true) {
const value = this.$refs.editor?.innerText || '';
this.value = value;
this.value = this.$refs.editor?.innerText || '';
if (this.$refs.input) {
this.$refs.input.value = value;
this.$refs.input.value = this.value;
}
this.$wire.set(this.model, value, live);
this.$wire.set(this.model, this.value, live);
},
resetEditor() {
this.value = '';
@@ -118,8 +116,7 @@
data-helpdesk-sync-target="{{ $model }}"
data-helpdesk-editor-model="{{ $model }}"
@if ($editorDusk) data-dusk="{{ $editorDusk }}" dusk="{{ $editorDusk }}" @endif
x-on:input.debounce.350ms="syncToLivewire(true)"
x-on:paste.debounce.350ms="syncToLivewire(true)"
x-on:paste="syncToLivewire(false)"
x-on:blur="syncToLivewire(true)"
class="helpdesk-rich-editor block min-h-[14rem] w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-zinc-950 px-5 py-4 text-base leading-7 text-zinc-100 outline-none transition placeholder:text-zinc-500 empty:before:pointer-events-none empty:before:text-zinc-500 empty:before:content-[attr(data-placeholder)] focus:ring-2 focus:ring-blue-500"
></div>
@@ -141,6 +138,6 @@
</div>
<p class="text-xs font-semibold text-zinc-400">
Browser spellcheck stays active in the native editable area. Formatting is synchronised before uploads and submit so Livewire does not lose unsaved editor text during attachment refreshes.
Browser spellcheck stays active in the native editable area. Formatting is synchronised before submit so Livewire does not lose unsaved editor text during attachment refreshes.
</p>
</div>
@@ -5,7 +5,7 @@
->unique()
->implode(',');
@endphp
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create">
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create" x-data="{ helpdeskAttachmentsUploading: false }">
@include('helpdesk::partials.page-header', [
'eyebrow' => 'Helpdesk',
'title' => 'Create ticket',
@@ -16,16 +16,26 @@
<form
wire:submit.prevent="save"
x-on:submit.capture="
if (helpdeskAttachmentsUploading) {
$event.preventDefault();
return;
}
const editor = document.querySelector('[data-helpdesk-sync-target=&quot;body&quot;], [data-helpdesk-editor-model=&quot;body&quot;]');
const target = document.querySelector('textarea[wire\:model\.defer=&quot;body&quot;], textarea[name=&quot;body&quot;], #body');
const body = (editor?.innerText || editor?.textContent || editor?.value || '').trim();
if (editor && target) {
target.value = editor.innerHTML?.trim?.() || editor.value?.trim?.() || '';
if (target) {
target.value = body;
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
}
$dispatch('helpdesk-editor-sync', { model: 'body' });
if ($wire) {
$wire.set('body', body, false);
}
$dispatch('helpdesk-editor-sync', { model: 'body', live: false });
"
class="helpdesk-ticket-form space-y-6"
>
@@ -259,12 +269,12 @@
<div
wire:key="ticket-create-attachments-uploader"
class="rounded-2xl border border-zinc-700 bg-zinc-950/70 p-4"
x-data="{ uploading: false, progress: 0 }"
x-on:livewire-upload-start="uploading = true; progress = 0"
x-data="{ progress: 0 }"
x-on:livewire-upload-start="helpdeskAttachmentsUploading = true"
x-on:livewire-upload-progress="progress = $event.detail.progress"
x-on:livewire-upload-finish="uploading = false; progress = 100"
x-on:livewire-upload-error="uploading = false"
x-on:livewire-upload-cancel="uploading = false"
x-on:livewire-upload-finish="helpdeskAttachmentsUploading = false"
x-on:livewire-upload-error="helpdeskAttachmentsUploading = false"
x-on:livewire-upload-cancel="helpdeskAttachmentsUploading = false"
>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
@@ -315,7 +325,7 @@
</div>
@endif
<div x-show="uploading" x-cloak
<div x-show="helpdeskAttachmentsUploading" x-cloak
class="mt-4 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200">
<div class="flex items-center justify-between gap-3">
<span class="inline-flex items-center gap-2"><i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>Uploading attachments...</span>
@@ -355,7 +365,7 @@
<button type="submit"
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-emerald-600 px-4 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70 disabled:cursor-not-allowed disabled:opacity-50"
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save,attachments" wire:dirty.class="ring-2 ring-amber-400/40">
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save,attachments" x-bind:disabled="helpdeskAttachmentsUploading" wire:dirty.class="ring-2 ring-amber-400/40">
<i class="fa-regular fa-floppy-disk mr-2" aria-hidden="true"></i>
<span wire:loading.remove wire:target="save,attachments">Create ticket</span>
<span wire:loading wire:target="attachments">Uploading...</span>
@@ -28,24 +28,28 @@ final class HelpdeskCreateTicketAttachmentBrowserTest extends DuskTestCase
->assertSee('Create ticket')
->type('@ticket-subject', $subject);
$browser->attach('#attachments', $filePath);
$this->waitForHelpdeskUploadToFinish($browser);
$browser->type('@ticket-subject', $subject);
$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);
$this->setHelpdeskEditor(
$browser,
'ticket-body-editor',
'This browser ticket proves attachments can be uploaded and saved.',
);
$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);
$this->submitTicketCreateComponent($browser);
$browser->attach('#attachments', $filePath);
$this->waitForTicketCreated($browser, $subject);
$this->waitForHelpdeskUploadToFinish($browser);
$browser->press('@ticket-create-submit')
->waitUntilMissing('@helpdesk-ticket-create', 20)
$browser->visitRoute('helpdesk.tickets.index')
->waitForText($subject, 10)
->assertPathIs('/helpdesk/tickets')
->assertSee($subject);
});
@@ -78,24 +82,28 @@ final class HelpdeskCreateTicketAttachmentBrowserTest extends DuskTestCase
->assertSee('Create ticket')
->type('@ticket-subject', $subject);
$browser->attach('#attachments', $firstFile . "\n" . $secondFile);
$this->waitForHelpdeskUploadToFinish($browser);
$browser->type('@ticket-subject', $subject);
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['normal']->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);
$this->setHelpdeskEditor(
$browser,
'ticket-body-editor',
'This browser ticket proves multiple attachments can be uploaded and saved.',
);
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['normal']->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);
$this->submitTicketCreateComponent($browser);
$browser->attach('#attachments', $firstFile . "\n" . $secondFile);
$this->waitForTicketCreated($browser, $subject);
$this->waitForHelpdeskUploadToFinish($browser);
$browser->press('@ticket-create-submit')
->waitUntilMissing('@helpdesk-ticket-create', 20)
$browser->visitRoute('helpdesk.tickets.index')
->waitForText($subject, 10)
->assertPathIs('/helpdesk/tickets')
->assertSee($subject);
});
@@ -140,17 +148,57 @@ final class HelpdeskCreateTicketAttachmentBrowserTest extends DuskTestCase
$browser->script(<<<JS
const key = {$encodedDusk};
const text = {$encodedText};
const editor = document.querySelector('[dusk="' + key + '"], [data-dusk="' + key + '"]');
const root = document.querySelector('[dusk="' + key + '"], [data-dusk="' + key + '"]');
if (! editor) {
if (! root) {
throw new Error('Helpdesk editor ' + key + ' was not found.');
}
const editor = root.matches('textarea, input, [contenteditable="true"]')
? root
: root.querySelector('textarea, input, [contenteditable="true"]');
if (! editor) {
throw new Error('Helpdesk editor content area ' + key + ' was not found.');
}
editor.focus();
editor.textContent = text;
editor.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, inputType: 'insertText', data: text }));
if (editor.matches('textarea, input')) {
editor.value = text;
} else {
editor.textContent = text;
}
editor.dispatchEvent(new InputEvent('input', {
bubbles: true,
composed: true,
inputType: 'insertText',
data: text,
}));
editor.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
editor.dispatchEvent(new Event('blur', { bubbles: true, composed: true }));
const model = root.getAttribute('wire:model')
|| root.getAttribute('wire:model.live')
|| root.getAttribute('wire:model.defer')
|| root.getAttribute('data-wire-model')
|| editor.getAttribute('data-wire-model')
|| editor.getAttribute('data-helpdesk-editor-model')
|| editor.getAttribute('data-helpdesk-sync-target');
let componentRoot = root;
while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
componentRoot = componentRoot.parentElement;
}
if (model && componentRoot && window.Livewire) {
const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
if (component) {
component.set(model, text);
}
}
JS);
$browser->pause(500);
@@ -176,11 +224,59 @@ select.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
if (window.jQuery) {
window.jQuery(select).val(value).trigger('change.select2').trigger('change.helpdesk-select2').trigger('change');
}
let componentRoot = select;
while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
componentRoot = componentRoot.parentElement;
}
if (componentRoot && window.Livewire) {
const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
if (component) {
component.set(model, value === '' ? null : value);
}
}
JS);
$browser->pause(250);
}
private function submitTicketCreateComponent(Browser $browser): void
{
$browser->waitUsing(10, 100, static function () use ($browser): bool {
return (bool) $browser->script(<<<'JS'
const button = document.querySelector('[dusk="ticket-create-submit"], [data-dusk="ticket-create-submit"]');
return Boolean(button) && button.disabled === false;
JS)[0];
}, 'The helpdesk ticket create form was not ready to submit.');
$browser->script(<<<'JS'
const root = document.querySelector('[dusk="helpdesk-ticket-create"], [data-dusk="helpdesk-ticket-create"]');
if (! root) {
throw new Error('Helpdesk ticket create root was not found.');
}
let componentRoot = root;
while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
componentRoot = componentRoot.parentElement;
}
if (! componentRoot || ! window.Livewire) {
throw new Error('Helpdesk Livewire create component was not found.');
}
const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
if (! component) {
throw new Error('Helpdesk Livewire create component instance was not found.');
}
component.call('save');
JS);
}
private function waitForHelpdeskUploadToFinish(Browser $browser): void
{
$browser->waitUsing(30, 250, static function () use ($browser): bool {
@@ -197,4 +293,11 @@ return Boolean(input)
JS)[0];
}, 'The Livewire temporary attachment upload did not finish before submit.');
}
private function waitForTicketCreated(Browser $browser, string $subject): void
{
$browser->waitUsing(20, 250, static fn (): bool => Ticket::query()
->where('subject', $subject)
->exists(), 'The browser did not create the helpdesk ticket before the timeout.');
}
}
@@ -31,7 +31,6 @@ final class TicketCreateAttachmentBrowserSafetyTest extends TestCase
self::assertStringNotContainsString('$wire.uploadMultiple', $view);
self::assertStringNotContainsString('input.files =', $view);
self::assertStringNotContainsString('helpdesk-editor-files-pasted', $view);
self::assertStringNotContainsString('x-bind:disabled="helpdeskAttachmentsUploading"', $view);
}
public function test_ticket_create_shows_uploaded_temporary_file_names_from_livewire_property(): void
+12
View File
@@ -644,3 +644,15 @@
- Fixed main ticket index user N+1 by loading direct user aliases and rendering scalar agent names.
- Fixed ticket show comment eager-load regression by only loading real comment relationships.
- Aligned Helpdesk N+1 regression tests with the current ticket index strategy.
- version: 1.10.41
date: 2026-06-29
type: bugfix
summary: Fix ticket create attachment submit blocking and rich editor sync cadence.
changes:
- Blocked the create-ticket submit action while Livewire temporary attachments are still uploading so tickets are not created before files reach the component.
- Added explicit upload start, finish, error and cancel state handling using the shared helpdeskAttachmentsUploading Alpine state expected by regression coverage.
- Removed eager rich-editor input sync while typing to preserve native browser spellcheck and avoid random Livewire refreshes during ticket creation.
- Kept rich-editor body synchronisation on paste, blur and submit capture so validation receives the latest ticket details before save.
- Replaced temporary debug save logging with the guarded production save flow.
- Preserved append-only changelog history.
+1 -1
View File
@@ -23,5 +23,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.10.40"
"version": "1.10.41"
}