refactor(outlet): menjadikan kode nya clean code

- menyesuaikan testing
- menghapus route untuk delete karena tidak lewat route
- menyesuaikan tipe prop dan lainnya
This commit is contained in:
Yoga Pangestu 2025-12-09 13:32:33 +07:00
parent 23a2bd580c
commit 977b191ccb
10 changed files with 813 additions and 284 deletions

View File

@ -28,21 +28,30 @@ class OutletForm extends Form
public ?string $maps_url = null;
/** @var array<string, array{open_time: ?string, close_time: ?string}> */
public array $opening_hours = [];
/** @var array<int, string> */
public array $facilities = ['Parkir'];
public string $opened_date = '';
public string $status = '1';
public int $status = OutletStatus::OPERATIONAL->value;
/** @var array<int, array<string, mixed>> */
public array $featured_image = [];
/** @var array<int, array<string, mixed>> */
public array $images = [];
public function rules(): array
{
$rules = [
return array_merge($this->getBasicRules(), $this->getOpeningHoursRules());
}
private function getBasicRules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'phone_number' => ['required', 'string', new PhoneNumber],
'address' => ['required', 'string'],
@ -60,6 +69,11 @@ public function rules(): array
'featured_image' => ['required', 'array', 'max:1'],
'images' => ['required', 'array', 'max:5'],
];
}
private function getOpeningHoursRules(): array
{
$rules = [];
foreach (Day::cases() as $day) {
$rules["opening_hours.{$day->value}"] = ['required', 'array'];
@ -88,18 +102,33 @@ public function validationAttributes(): array
];
}
public function setOutlet(Outlet $outlet)
public function setOutlet(Outlet $outlet): void
{
$outlet = $outlet->load(['openingHours', 'facilities']);
$outlet->load(['openingHours', 'facilities']);
$this->outlet = $outlet;
$this->fillBasicAttributes($outlet);
$this->opening_hours = $this->formatOpeningHoursForForm($outlet);
$this->facilities = $outlet->facilities->pluck('name')->toArray();
$this->featured_image = $this->mapMediaCollection($outlet->getMedia('featured_image'));
$this->images = $this->mapMediaCollection($outlet->getMedia('images'));
}
private function fillBasicAttributes(Outlet $outlet): void
{
$this->name = $outlet->name;
$this->phone_number = $outlet->phone_number;
$this->address = $outlet->address;
$this->landmark = $outlet->landmark;
$this->maps_url = $outlet->maps_url;
$this->opening_hours = $outlet->openingHours
$this->opened_date = $outlet->opened_date;
$this->status = $outlet->status->value;
}
private function formatOpeningHoursForForm(Outlet $outlet): array
{
return $outlet->openingHours
->mapWithKeys(fn ($item) => [
$item->day->value => [
'open_time' => $item->open_time ? formatTime($item->open_time) : null,
@ -107,88 +136,121 @@ public function setOutlet(Outlet $outlet)
],
])
->toArray();
$this->facilities = $outlet->facilities->pluck('name')->toArray();
$this->opened_date = $outlet->opened_date;
$this->status = $outlet->status->value;
$this->featured_image = $this->mapMediaCollection($outlet->getMedia('featured_image'));
$this->images = $this->mapMediaCollection($outlet->getMedia('images'));
}
public function store()
public function store(): void
{
$this->validate();
$data = $this->all();
if ($data['status'] == OutletStatus::TERMINATED) {
$data['closed_date'] = now();
} else {
$data['closed_date'] = null;
}
$data = $this->prepareDataForSave();
DB::transaction(function () use ($data) {
$outlet = Outlet::create($data);
$openingHours = collect($data['opening_hours'])
->map(fn ($time, $day) => [
'outlet_id' => $outlet->id,
'day' => $day,
'open_time' => $time['open_time'],
'close_time' => $time['close_time'],
])
->values()
->toArray();
OpeningHour::insert($openingHours);
$outlet->facilities()->createMany(collect($data['facilities'])->map(fn ($facility) => ['name' => $facility])->toArray());
$this->uploadMedia($this->featured_image, $outlet, 'featured_image');
$this->uploadMedia($this->images, $outlet, 'images');
$this->createOpeningHours($outlet, $data['opening_hours']);
$this->createFacilities($outlet, $data['facilities']);
$this->handleMediaUpload($outlet);
});
}
public function update()
private function prepareDataForSave(): array
{
$data = $this->all();
$data['closed_date'] = $data['status'] == OutletStatus::TERMINATED->value
? now()
: null;
return $data;
}
private function createOpeningHours(Outlet $outlet, array $openingHours): void
{
$hoursData = collect($openingHours)
->map(fn ($time, $day) => [
'outlet_id' => $outlet->id,
'day' => $day,
'open_time' => $time['open_time'],
'close_time' => $time['close_time'],
])
->values()
->toArray();
OpeningHour::insert($hoursData);
}
private function createFacilities(Outlet $outlet, array $facilities): void
{
$facilitiesData = collect($facilities)
->map(fn ($facility) => ['name' => $facility])
->toArray();
$outlet->facilities()->createMany($facilitiesData);
}
private function handleMediaUpload(Outlet $outlet): void
{
$this->uploadMedia($this->featured_image, $outlet, 'featured_image');
$this->uploadMedia($this->images, $outlet, 'images');
}
public function update(): void
{
$this->validate();
$data = $this->all();
if ($data['status'] == OutletStatus::TERMINATED->value) {
$data['closed_date'] = now()->toDateString();
} else {
$data['closed_date'] = null;
}
$data = $this->prepareDataForSave();
DB::transaction(function () use ($data) {
$this->outlet->update($data);
foreach ($data['opening_hours'] as $day => $hours) {
OpeningHour::updateOrCreate(
[
'outlet_id' => $this->outlet->id,
'day' => $day,
],
[
'open_time' => $hours['open_time'] ?? null,
'close_time' => $hours['close_time'] ?? null,
]
);
}
$facilities = collect($data['facilities']);
foreach ($facilities as $facilityName) {
$this->outlet->facilities()->updateOrCreate(
['name' => $facilityName],
['name' => $facilityName]
);
}
$this->syncMedia($data['featured_image'], $this->outlet, 'featured_image');
$this->syncMedia($data['images'], $this->outlet, 'images');
$this->uploadMedia($this->featured_image, $this->outlet, 'featured_image');
$this->uploadMedia($this->images, $this->outlet, 'images');
$this->updateOpeningHours($data['opening_hours']);
$this->updateFacilities($data['facilities']);
$this->handleMediaSync($data);
$this->handleMediaUpload($this->outlet);
});
}
private function updateOpeningHours(array $openingHours): void
{
// Delete opening hours for days that are not in the new data
$this->outlet->openingHours()
->whereNotIn('day', array_keys($openingHours))
->delete();
// Add or update opening hours
foreach ($openingHours as $day => $hours) {
OpeningHour::updateOrCreate(
[
'outlet_id' => $this->outlet->id,
'day' => $day,
],
[
'open_time' => $hours['open_time'] ?? null,
'close_time' => $hours['close_time'] ?? null,
]
);
}
}
private function updateFacilities(array $facilities): void
{
// Delete facilities that are not in the new list
$this->outlet->facilities()
->whereNotIn('name', $facilities)
->delete();
// Add or update facilities
foreach ($facilities as $facilityName) {
$this->outlet->facilities()->updateOrCreate(
['name' => $facilityName],
['name' => $facilityName]
);
}
}
private function handleMediaSync(array $data): void
{
$this->syncMedia($data['featured_image'], $this->outlet, 'featured_image');
$this->syncMedia($data['images'], $this->outlet, 'images');
}
}

View File

@ -7,30 +7,35 @@
use App\Traits\Outlet\WithFacilityHandler;
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Tambah Outlet')]
class Create extends Component
{
use WithAuthorization, WithFacilityHandler, WithToast, WithUpdatedData;
use WithAuthorization, WithFacilityHandler, WithToast;
public OutletForm $form;
/** @var array<int, Day> */
public array $days = [];
public function mount()
public function mount(): void
{
$this->form->opening_hours = collect(Day::cases())
->mapWithKeys(fn (Day $day) => [
$day->value => ['open_time' => null, 'close_time' => null],
$day->value => [
'open_time' => null,
'close_time' => null,
],
])
->toArray();
$this->days = Day::cases();
}
public function save()
public function save(): void
{
$this->canOrAbort('create outlet');
@ -41,7 +46,7 @@ public function save()
$this->redirectRoute('studio.master.outlet.index');
}
public function render()
public function render(): View
{
return view('livewire.studio.master.outlet.form', [
'pageTitle' => 'Tambah Outlet',

View File

@ -8,27 +8,27 @@
use App\Traits\Outlet\WithFacilityHandler;
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Ubah Outlet')]
class Edit extends Component
{
use WithAuthorization, WithFacilityHandler, WithToast, WithUpdatedData;
use WithAuthorization, WithFacilityHandler, WithToast;
public OutletForm $form;
/** @var array<int, Day> */
public array $days = [];
public function mount(Outlet $outlet)
public function mount(Outlet $outlet): void
{
$this->form->setOutlet($outlet);
$this->days = Day::cases();
}
public function save()
public function save(): void
{
$this->canOrAbort('update outlet');
@ -39,7 +39,7 @@ public function save()
$this->redirectRoute('studio.master.outlet.index');
}
public function render()
public function render(): View
{
return view('livewire.studio.master.outlet.form', [
'pageTitle' => 'Ubah Outlet',

View File

@ -4,98 +4,92 @@
use App\Enums\Day;
use App\Models\Outlet;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\WithCloseModal;
use App\Traits\WithConfirmation;
use App\Traits\WithMediaHandler;
use App\Traits\WithToast;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Outlet')]
class Index extends Component
{
use WithCloseModal, WithConfirmation, WithMediaHandler, WithSubscribeNotification, WithToast;
use WithCloseModal, WithConfirmation, WithMediaHandler, WithToast;
public array $outlets = [];
public ?string $imageUrl = null;
/** @var Collection<int, Outlet> */
public Collection $outlets;
public string $search = '';
public array $status = [];
public function mount()
public function mount(): void
{
$this->loadOutlets();
}
protected function loadOutlets()
protected function loadOutlets(): void
{
$this->outlets = Outlet::with(['openingHours', 'facilities'])
->when(! empty($this->search), fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when(! empty($this->status), fn ($query) => $query->whereIn('status', $this->status))
->latest()
->get()
->map(fn ($outlet) => [
'hash' => $outlet->hash,
'name' => $outlet->name,
'phone_number' => $outlet->phone_number,
'landmark' => $outlet->landmark,
'address' => $outlet->address,
'status' => [
'label' => $outlet->status->label(),
'color' => $outlet->status->color(),
],
'opening_hours' => $outlet->openingHours->mapWithKeys(fn ($hour) => [
Str::ucfirst(Str::lower(Day::from($hour->day->value)->label())) => [
'open' => formatTime($hour->open_time),
'close' => formatTime($hour->close_time),
'is_closed' => $hour->open_time === null && $hour->close_time === null,
],
])->toArray(),
'facilities' => $outlet->facilities->pluck('name')->toArray(),
'opened_date' => formatDate($outlet->opened_date),
'opened_ago' => timeAgo($outlet->opened_date),
'closed_date' => formatDate($outlet->closed_date),
'closed_ago' => timeAgo($outlet->closed_date),
'image' => data_get($this->mapMediaCollection($outlet->getMedia('featured_image'))[0] ?? null, 'temporaryUrl', asset('assets/images/logo.png')),
'images' => collect($this->mapMediaCollection($outlet->getMedia('images')))->pluck('temporaryUrl')->filter()->values(),
])
->get();
}
protected function formatOpeningHours(Outlet $outlet): array
{
return $outlet->openingHours->mapWithKeys(fn ($hour) => [
Str::ucfirst(Str::lower(Day::from($hour->day->value)->label())) => [
'open' => formatTime($hour->open_time),
'close' => formatTime($hour->close_time),
'is_closed' => $hour->open_time === null && $hour->close_time === null,
],
])->toArray();
}
protected function getFeaturedImage(Outlet $outlet): string
{
$mediaCollection = $this->mapMediaCollection($outlet->getMedia('featured_image'));
return data_get($mediaCollection[0] ?? null, 'temporaryUrl', asset('assets/images/logo.png'));
}
protected function getGalleryImages(Outlet $outlet): array
{
return collect($this->mapMediaCollection($outlet->getMedia('images')))
->pluck('temporaryUrl')
->filter()
->values()
->toArray();
}
public function updatedSearch(string $value)
public function updatedSearch(string $value): void
{
$this->search = $value;
$this->loadOutlets();
}
public function updatedStatus()
public function updatedStatus(): void
{
$this->loadOutlets();
}
public function openImage(string $imageUrl)
public function delete(Outlet $outlet): void
{
$this->imageUrl = $imageUrl;
$this->authorize('delete outlet');
Flux::modal('image-modal')->show();
}
public function delete(Outlet $outlet)
{
$outlet->delete();
$this->toast('Outlet berhasil dihapus.');
$this->loadOutlets();
Flux::modals()->close();
}
public function render()
public function render(): View
{
return view('livewire.studio.master.outlet.index', [
'pageTitle' => 'Outlet',

View File

@ -34,14 +34,13 @@ class="text-sm">
class="space-y-2 transition-transform duration-300 ease-out hover:-translate-y-1 hover:shadow-lg">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-3">
<img src="{{ $outlet['image'] }}" alt="{{ $outlet['image'] }}"
class="w-12 h-12 rounded-full cursor-pointer"
wire:click="openImage('{{ $outlet['image'] }}')">
<img src="{{ $this->getFeaturedImage($outlet) }}" alt="{{ $outlet->name }}"
class="w-12 h-12 rounded-full">
<div>
<flux:heading size="lg">{{ $outlet['name'] }}</flux:heading>
<flux:badge color="{{ $outlet['status']['color'] }}" size="sm">
{{ $outlet['status']['label'] }}
<flux:heading size="lg">{{ $outlet->name }}</flux:heading>
<flux:badge color="{{ $outlet->status->color() }}" size="sm">
{{ $outlet->status->label() }}
</flux:badge>
</div>
</div>
@ -50,14 +49,14 @@ class="w-12 h-12 rounded-full cursor-pointer"
<div class="space-y-3 mb-4">
<div class="flex items-center gap-2 text-sm">
<flux:icon.phone class="w-4 h-4 text-gray-500 dark:text-white" />
<span class="text-gray-700 dark:text-gray-300">{{ $outlet['phone_number'] }}</span>
<span class="text-gray-700 dark:text-gray-300">{{ $outlet->phone_number }}</span>
</div>
<div class="flex items-start gap-2 text-sm">
<flux:icon.map-pin class="w-4 h-4 text-gray-500 dark:text-white" />
<div class="text-gray-700 dark:text-gray-300">
<div class="font-medium">{{ $outlet['landmark'] }}</div>
<div class="text-gray-600 dark:text-gray-400">{{ $outlet['address'] }}</div>
<div class="font-medium">{{ $outlet->landmark }}</div>
<div class="text-gray-600 dark:text-gray-400">{{ $outlet->address }}</div>
</div>
</div>
</div>
@ -69,7 +68,7 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
Jam Operasional
</h4>
<div class="space-y-1 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
@foreach ($outlet['opening_hours'] as $day => $time)
@foreach ($this->formatOpeningHours($outlet) as $day => $time)
<div class="flex justify-between text-xs">
<span
class="font-medium text-gray-700 dark:text-gray-300">{{ $day }}:</span>
@ -88,8 +87,8 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
Fasilitas
</h4>
<div class="flex flex-wrap gap-2">
@foreach ($outlet['facilities'] as $facility)
<flux:badge color="zinc" size="sm">{{ $facility }}</flux:badge>
@foreach ($outlet->facilities as $facility)
<flux:badge color="zinc" size="sm">{{ $facility->name }}</flux:badge>
@endforeach
</div>
</div>
@ -98,21 +97,22 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
<div class="flex flex-col gap-2 text-xs">
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl Buka:</span>
<span class="text-gray-700 dark:text-gray-300">{{ $outlet['opened_date'] }}</span>
<span class="text-gray-500">({{ $outlet['opened_ago'] }})</span>
<span
class="text-gray-700 dark:text-gray-300">{{ formatDate($outlet->opened_date) }}</span>
<span class="text-gray-500">({{ timeAgo($outlet->opened_date) }})</span>
</div>
@if ($outlet['closed_date'])
@if ($outlet->closed_date)
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl Tutup:</span>
<span
class="text-gray-700 dark:text-gray-300">{{ $outlet['closed_date'] }}</span>
<span class="text-gray-500">({{ $outlet['closed_ago'] }})</span>
class="text-gray-700 dark:text-gray-300">{{ formatDate($outlet->closed_date) }}</span>
<span class="text-gray-500">({{ timeAgo($outlet->closed_date) }})</span>
</div>
@endif
</div>
</div>
@if (count($outlet['images']) > 0)
@if ($this->getGalleryImages($outlet))
<div class="mb-4">
<h4
class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
@ -120,10 +120,9 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
Gambar
</h4>
<div class="flex mt-6">
@foreach ($outlet['images'] as $key => $value)
<img src="{{ $value }}" alt="{{ $value }}"
class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
wire:click="openImage('{{ $value }}')">
@foreach ($this->getGalleryImages($outlet) as $imageUrl)
<img src="{{ $imageUrl }}" alt="{{ $outlet->name }}"
class="w-12 h-12 rounded-full -ml-3 first:ml-0">
@endforeach
</div>
</div>
@ -134,7 +133,7 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
@can('update outlet')
<flux:tooltip content="Ubah">
<flux:button href="{{ route('studio.master.outlet.edit', $outlet['hash']) }}"
<flux:button href="{{ route('studio.master.outlet.edit', $outlet->hash) }}"
wire:navigate.hover variant="primary" color="yellow" icon="pencil-square"
size="sm">
</flux:button>
@ -142,10 +141,10 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
@endcan
@can('delete outlet')
<flux:modal.trigger name="delete">
<flux:modal.trigger name="delete-confirmation">
<flux:tooltip content="Hapus">
<flux:button variant="danger" icon="trash" size="sm"
wire:click="$dispatch('fn:confirmAction', {id: '{{ $outlet['hash'] }}'})">
wire:click="$dispatch('fn:confirmAction', {id: '{{ $outlet->hash }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@ -156,19 +155,18 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
@endforeach
</div>
@else
@include('components.lottie.not-found')
@include('components.animations.lottie.not-found')
@endif
</div>
@include('components.confirmation.delete')
<flux:modal name="image-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
<div class="flex justify-center items-center p-4">
<img src="{{ $imageUrl }}" alt="Outlet Image" class="rounded-lg max-h-[80vh] object-contain" />
</div>
</flux:modal>
@include('components.modals.confirmation', [
'modalName' => 'delete-confirmation',
'modalTitle' => 'Apakah Anda yakin?',
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
'buttonVariant' => 'primary',
'buttonColor' => 'danger',
'buttonText' => 'Ya, Hapus',
])
</flux:main>
@assets

View File

@ -66,7 +66,6 @@
Route::get('/', OutletIndex::class)->name('index')->middleware('can:view outlet');
Route::get('/create', OutletCreate::class)->name('create')->middleware('can:create outlet');
Route::get('/{outlet}/edit', OutletEdit::class)->name('edit')->middleware('can:update outlet');
Route::delete('/{outlet}/delete', OutletCreate::class)->name('delete')->middleware('can:delete outlet');
});
Route::prefix('users')->name('user.')->group(function () {

View File

@ -1,6 +1,9 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Enums\Day;
use App\Enums\OutletStatus;
use App\Livewire\Studio\Master\Outlet\Create;
use App\Livewire\Studio\Master\Outlet\Index;
use App\Models\Employee;
@ -8,48 +11,19 @@
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->map(fn ($role) => Role::create(['name' => $role]));
$this->user = User::factory()
->active()
->has(Employee::factory())
// ->has(Outlet::factory()->count(5))
->create();
$this->user->roles()->attach($roles->pluck('id'));
});
function makeLivewireFile(string $name)
{
$file = UploadedFile::fake()->image($name, 100, 100);
$tmpId = Str::random(32);
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
$path = "livewire-tmp/{$filename}";
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
return [
'tmpFilename' => $filename,
'name' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
'path' => storage_path("app/private/{$path}"),
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
'size' => $file->getSize(),
];
}
function mountCreateComponent(User $user)
{
return Livewire::actingAs($user)->test(Create::class);
@ -156,8 +130,6 @@ function mountCreateComponent(User $user)
'form.phone_number',
'form.address',
'form.landmark',
'form.opening_hours',
'form.facilities',
'form.opened_date',
'form.featured_image',
'form.images',
@ -220,6 +192,17 @@ function mountCreateComponent(User $user)
expect($component->form->facilities)->toBe(['Parkir']);
});
it('initializes opening hours for all days of week', function () {
$component = mountCreateComponent($this->user);
foreach (Day::cases() as $day) {
expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue();
expect($component->form->opening_hours[$day->value])->toHaveKeys(['open_time', 'close_time']);
expect($component->form->opening_hours[$day->value]['open_time'])->toBeNull();
expect($component->form->opening_hours[$day->value]['close_time'])->toBeNull();
}
});
it('creates outlet with opening hours for all days', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
@ -253,4 +236,190 @@ function mountCreateComponent(User $user)
$createdOutlet = Outlet::first();
expect($createdOutlet->openingHours)->toHaveCount(count(Day::cases()));
// Verify opening hours data
foreach ($createdOutlet->openingHours as $openingHour) {
expect($openingHour->open_time)->toBe('09:00');
expect($openingHour->close_time)->toBe('21:00');
}
});
it('creates outlet with facilities', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$facilities = ['Parkir', 'WiFi', 'AC'];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', $outlet['status']->value)
->set('form.facilities', $facilities)
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->facilities->pluck('name')->toArray())->toBe($facilities);
});
it('sets closed_date when status is TERMINATED', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', OutletStatus::TERMINATED->value)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->status->value)->toBe(OutletStatus::TERMINATED->value);
expect($createdOutlet->closed_date)->not->toBeNull();
});
it('does not set closed_date for non-terminated status', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', OutletStatus::OPERATIONAL->value)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->closed_date)->toBeNull();
});
it('can add and remove facilities dynamically', function () {
$component = mountCreateComponent($this->user);
// Initial state
expect($component->form->facilities)->toBe(['Parkir']);
// Add facility
$component->call('addFacility');
expect($component->form->facilities)->toBe(['Parkir', '']);
// Add another facility
$component->call('addFacility');
expect($component->form->facilities)->toBe(['Parkir', '', '']);
// Remove middle facility
$component->call('removeFacility', 1);
expect($component->form->facilities)->toBe(['Parkir', '']);
// Remove first facility (should keep at least one empty)
$component->call('removeFacility', 0);
expect($component->form->facilities)->toBe(['']);
});
it('validates maps_url regex pattern', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.maps_url', 'invalid-url')
->set('form.opened_date', $outlet['opened_date'])
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasErrors(['form.maps_url']);
// Valid Google Maps URL should pass
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.maps_url', 'https://maps.app.goo.gl/example')
->set('form.opened_date', $outlet['opened_date'])
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
});
it('handles facility removal edge cases', function () {
$component = mountCreateComponent($this->user);
// Set custom facilities
$component->set('form.facilities', ['Parkir', 'WiFi', 'AC']);
// Remove middle facility
$component->call('removeFacility', 1);
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
// Try to remove non-existent index (should not crash)
$component->call('removeFacility', 99);
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
});
it('creates outlet with soft delete capability', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet)->toBeInstanceOf(Outlet::class);
// Verify soft delete trait
$createdOutlet->delete();
expect(Outlet::withTrashed()->find($createdOutlet->id))->not->toBeNull();
expect(Outlet::find($createdOutlet->id))->toBeNull();
});

View File

@ -1,44 +1,30 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Enums\Day;
use App\Enums\OutletStatus;
use App\Livewire\Studio\Master\Outlet\Edit;
use App\Livewire\Studio\Master\Outlet\Index;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
});
function makeLivewireFile(string $name)
{
$file = UploadedFile::fake()->image($name, 100, 100);
$tmpId = Str::random(32);
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
$path = "livewire-tmp/{$filename}";
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
return [
'tmpFilename' => $filename,
'name' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
'path' => storage_path("app/private/{$path}"),
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
'size' => $file->getSize(),
];
}
function mountEditComponent(User $user, Outlet $outlet)
{
return Livewire::actingAs($user)->test(Edit::class, ['outlet' => $outlet]);
@ -132,3 +118,229 @@ function mountEditComponent(User $user, Outlet $outlet)
expect($media->getPath())->toBeFile();
}
});
it('loads outlet data into form correctly', function () {
$outlet = Outlet::factory()
->operational()
->has(OpeningHour::factory()->count(7))
->has(Facility::factory()->count(3))
->create();
$component = mountEditComponent($this->user, $outlet);
expect($component->form->outlet->id)->toBe($outlet->id);
expect($component->form->name)->toBe($outlet->name);
expect($component->form->phone_number)->toBe($outlet->phone_number);
expect($component->form->address)->toBe($outlet->address);
expect($component->form->landmark)->toBe($outlet->landmark);
expect($component->form->status)->toBe($outlet->status->value);
expect($component->form->facilities)->toBe($outlet->facilities->pluck('name')->toArray());
});
it('loads opening hours into form correctly', function () {
$outlet = Outlet::factory()->operational()->create();
// Create specific opening hours
$openingHours = [];
foreach (Day::cases() as $day) {
$openingHours[] = OpeningHour::factory()->create([
'outlet_id' => $outlet->id,
'day' => $day,
'open_time' => '09:00',
'close_time' => '21:00',
]);
}
$component = mountEditComponent($this->user, $outlet);
foreach (Day::cases() as $day) {
expect($component->form->opening_hours[$day->value]['open_time'])->toBe('09:00');
expect($component->form->opening_hours[$day->value]['close_time'])->toBe('21:00');
}
});
it('updates outlet with partial data changes', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()
->operational()
->has(Facility::factory()->count(2))
->create();
$newName = 'Updated Outlet Name';
$images = [makeLivewireFile('image1.jpg')];
$featuredImage = makeLivewireFile('featured.jpg');
mountEditComponent($this->user, $outlet)
->set('form.name', $newName)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors()
->assertRedirect(Index::class);
$outlet->refresh();
expect($outlet->name)->toBe($newName);
expect($outlet->phone_number)->toBe($outlet->phone_number); // Unchanged
});
it('updates outlet status to TERMINATED and sets closed_date', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.status', OutletStatus::TERMINATED->value)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->status->value)->toBe(OutletStatus::TERMINATED->value);
expect($outlet->closed_date)->not->toBeNull();
});
it('updates facilities correctly', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()
->operational()
->has(Facility::factory()->count(2))
->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$newFacilities = ['WiFi', 'AC', 'Parking'];
mountEditComponent($this->user, $outlet)
->set('form.facilities', $newFacilities)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->facilities->pluck('name')->toArray())->toBe($newFacilities);
});
it('updates opening hours correctly', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$newOpeningHours = makeValidOpeningHours();
mountEditComponent($this->user, $outlet)
->set('form.opening_hours', $newOpeningHours)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->openingHours)->toHaveCount(count(Day::cases()));
foreach ($outlet->openingHours as $openingHour) {
expect($openingHour->open_time)->toBe('09:00');
expect($openingHour->close_time)->toBe('21:00');
}
});
it('validates required fields on update', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
mountEditComponent($this->user, $outlet)
->set('form.name', '') // Empty required field
->call('save')
->assertHasErrors(['form.name']);
});
it('maintains outlet relationships integrity', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()
->operational()
->has(Facility::factory()->count(3))
->create();
$originalOpeningHoursCount = count(Day::cases()); // Should be 7 after setOutlet
$originalFacilitiesCount = 1; // We set facilities to ['Parkir'] in the test
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.name', 'Updated Name')
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->openingHours)->toHaveCount($originalOpeningHoursCount);
expect($outlet->facilities)->toHaveCount($originalFacilitiesCount);
});
it('handles outlet with no existing media', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->getMedia('featured_image'))->toHaveCount(1);
});
it('preserves outlet slug when updating', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$originalSlug = $outlet->slug;
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.phone_number', '0812 3456 7890') // Change non-slug field
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->slug)->toBe($originalSlug);
});

View File

@ -7,26 +7,23 @@
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->map(fn ($role) => Role::create(['name' => $role]));
$this->user = User::factory()
->active()
->has(Employee::factory())
// ->has(Outlet::factory()->count(5))
->create();
$this->user->roles()->attach($roles->pluck('id'));
});
function createOutlet(array $attributes = [])
function createOutlet(array $attributes = []): Outlet
{
return Outlet::factory()
->has(OpeningHour::factory()->count(7))
@ -37,7 +34,7 @@ function createOutlet(array $attributes = [])
->create();
}
function mountIndexComponent(User $user)
function mountIndexComponent(User $user): Testable
{
return Livewire::actingAs($user)->test(Index::class);
}
@ -48,23 +45,24 @@ function mountIndexComponent(User $user)
->assertViewHas('pageTitle', 'Outlet');
});
it('mounts outlets correctly', function () {
it('loads outlets as Eloquent collection', function () {
$outlet = createOutlet();
mountIndexComponent($this->user)
->assertViewHas('outlets', function ($outlets) use ($outlet) {
return collect($outlets)->contains(
fn ($o) => $o['name'] === $outlet->name &&
$o['phone_number'] === $outlet->phone_number
);
});
});
it('opens image modal when openImage is called', function () {
$component = mountIndexComponent($this->user);
$url = 'https://example.com/test.jpg';
$component->call('openImage', $url)->assertSet('imageUrl', $url);
expect($component->get('outlets'))
->toBeInstanceOf(Collection::class)
->and($component->get('outlets')->first()->id)->toBe($outlet->id);
});
it('loads outlets with relationships', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
$loadedOutlet = $component->get('outlets')->first();
expect($loadedOutlet->openingHours)->toHaveCount(7)
->and($loadedOutlet->facilities)->toHaveCount(5);
});
it('displays all outlets', function () {
@ -72,15 +70,25 @@ function mountIndexComponent(User $user)
createOutlet();
}
$outletCount = Outlet::count();
$firstOutlet = Outlet::first();
$component = mountIndexComponent($this->user);
mountIndexComponent($this->user)
->assertSee($firstOutlet->name)
->assertViewHas('outlets', fn ($outlets) => count($outlets) === $outletCount);
expect($component->get('outlets'))->toHaveCount(5);
expect($component->get('outlets')->first())->toBeInstanceOf(Outlet::class);
});
it('deletes an outlet successfully', function () {
it('prevents delete when user is unauthorized', function () {
$outlet = createOutlet();
Gate::define('delete outlet', fn () => false);
mountIndexComponent($this->user)
->call('delete', $outlet)
->assertForbidden();
});
it('deletes an outlet successfully when authorized', function () {
$permission = Permission::create(['name' => 'delete outlet']);
$this->user->givePermissionTo($permission);
$outlet = createOutlet();
mountIndexComponent($this->user)
@ -97,18 +105,19 @@ function mountIndexComponent(User $user)
$component = mountIndexComponent($this->user);
expect(count($component->get('outlets')))->toBe(3);
expect($component->get('outlets'))->toHaveCount(3);
$component->set('search', 'Jakarta')
->assertSet('search', 'Jakarta');
// Test filtering by Jakarta
$component->set('search', 'Jakarta');
$filteredComponent = mountIndexComponent($this->user)->set('search', 'Jakarta');
expect(count($component->get('outlets')))->toBe(1);
expect($component->get('outlets.0.name'))->toBe('Outlet Jakarta');
expect($filteredComponent->get('outlets'))->toHaveCount(1);
expect($filteredComponent->get('outlets')->first()->name)->toBe('Outlet Jakarta');
$component->set('search', 'Outlet')
->assertSet('search', 'Outlet');
// Test filtering by Outlet
$filteredComponent2 = mountIndexComponent($this->user)->set('search', 'Outlet');
expect(count($component->get('outlets')))->toBe(2);
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
});
it('filters outlets by status', function () {
@ -118,20 +127,20 @@ function mountIndexComponent(User $user)
$component = mountIndexComponent($this->user);
expect(count($component->get('outlets')))->toBe(3);
expect($component->get('outlets'))->toHaveCount(3);
$component->set('status', [OutletStatus::OPERATIONAL->value])
->assertSet('status', [OutletStatus::OPERATIONAL->value]);
$filteredComponent = mountIndexComponent($this->user)->set('status', [OutletStatus::OPERATIONAL->value]);
expect(count($component->get('outlets')))->toBe(1);
expect($component->get('outlets.0.name'))->toBe($outlet1->name);
expect($filteredComponent->get('outlets'))->toHaveCount(1);
expect($filteredComponent->get('outlets')->first()->name)->toBe($outlet1->name);
$component->set('status', [
// Test filtering by multiple statuses
$filteredComponent2 = mountIndexComponent($this->user)->set('status', [
OutletStatus::OPERATIONAL->value,
OutletStatus::UNDER_CONSTRUCTION->value,
]);
expect(count($component->get('outlets')))->toBe(2);
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
});
it('filters outlets by both search and status', function () {
@ -143,35 +152,19 @@ function mountIndexComponent(User $user)
->set('search', 'Outlet')
->set('status', [OutletStatus::OPERATIONAL->value]);
expect(count($component->get('outlets')))->toBe(2);
expect($component->get('outlets'))->toHaveCount(2);
});
it('loads outlet data with all required fields', function () {
it('loads outlets with all required relationships', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
$outletData = $component->get('outlets.0');
$loadedOutlet = $component->get('outlets')->first();
expect($outletData)->toHaveKeys([
'hash',
'name',
'phone_number',
'landmark',
'address',
'status',
'opening_hours',
'facilities',
'opened_date',
'opened_ago',
'closed_date',
'closed_ago',
'image',
'images',
]);
expect($outletData['status'])->toHaveKeys(['label', 'color']);
expect($outletData['name'])->toBe($outlet->name);
expect($outletData['phone_number'])->toBe($outlet->phone_number);
expect($loadedOutlet->openingHours)->toHaveCount(7)
->and($loadedOutlet->facilities)->toHaveCount(5)
->and($loadedOutlet->name)->toBe($outlet->name)
->and($loadedOutlet->phone_number)->toBe($outlet->phone_number);
});
it('loads outlets ordered by latest', function () {
@ -181,6 +174,66 @@ function mountIndexComponent(User $user)
$component = mountIndexComponent($this->user);
expect($component->get('outlets.0.name'))->toBe('Second');
expect($component->get('outlets.1.name'))->toBe('First');
expect($component->get('outlets')->first()->name)->toBe('Second');
expect($component->get('outlets')->last()->name)->toBe('First');
});
it('provides helper methods for view formatting', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
// Test that outlets are loaded with proper data structure
$loadedOutlet = $component->get('outlets')->first();
expect($loadedOutlet)->toBeInstanceOf(Outlet::class);
expect($loadedOutlet->openingHours)->toBeInstanceOf(Collection::class);
expect($loadedOutlet->facilities)->toBeInstanceOf(Collection::class);
});
it('handles empty search results gracefully', function () {
createOutlet(['name' => 'Existing Outlet']);
$component = mountIndexComponent($this->user)->set('search', 'NonExistentOutlet');
expect($component->get('outlets'))->toHaveCount(0);
});
it('maintains search state across component lifecycle', function () {
$outlet1 = createOutlet(['name' => 'Test Outlet']);
$outlet2 = createOutlet(['name' => 'Another Outlet']);
$component = mountIndexComponent($this->user)->set('search', 'Test');
expect($component->get('outlets'))->toHaveCount(1);
expect($component->get('search'))->toBe('Test');
// Re-mount should maintain state
$newComponent = mountIndexComponent($this->user)->set('search', 'Test');
expect($newComponent->get('outlets'))->toHaveCount(1);
});
it('handles status filter with empty array', function () {
createOutlet(['status' => OutletStatus::OPERATIONAL]);
createOutlet(['status' => OutletStatus::TERMINATED]);
$component = mountIndexComponent($this->user)->set('status', []);
expect($component->get('outlets'))->toHaveCount(2);
});
it('loads outlets efficiently with eager loading', function () {
$outlets = collect();
for ($i = 0; $i < 10; $i++) {
$outlets->push(createOutlet());
}
$component = mountIndexComponent($this->user);
// Verify relationships are loaded
$loadedOutlets = $component->get('outlets');
foreach ($loadedOutlets as $loadedOutlet) {
expect($loadedOutlet->relationLoaded('openingHours'))->toBeTrue();
expect($loadedOutlet->relationLoaded('facilities'))->toBeTrue();
}
});

View File

@ -0,0 +1,37 @@
<?php
use App\Enums\Day;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
function makeLivewireFile(string $name)
{
$file = UploadedFile::fake()->image($name, 100, 100);
$tmpId = Str::random(32);
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
$path = "livewire-tmp/{$filename}";
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
return [
'tmpFilename' => $filename,
'name' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
'path' => storage_path("app/private/{$path}"),
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
'size' => $file->getSize(),
];
}
function makeValidOpeningHours()
{
return collect(Day::cases())
->mapWithKeys(fn ($day) => [
$day->value => [
'open_time' => '09:00',
'close_time' => '21:00',
],
])
->toArray();
}