From 39c0652e3e7dd5204804c224b58cd4c257da2c78 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 2 Jan 2026 16:37:25 +0700 Subject: [PATCH] feat: Add comprehensive feature tests for outlet CRUD operations and refactor outlet index data loading to fetch data directly in the render method. --- app/Livewire/Studio/Master/Outlet/Index.php | 36 ++----- .../Studio/Master/Outlet/CreateTest.php | 96 +++++++++++++++++++ .../Feature/Studio/Master/Outlet/EditTest.php | 60 ++++++++++++ .../Studio/Master/Outlet/IndexTest.php | 78 +++++++++++++++ tests/Pest.php | 27 ++++++ 5 files changed, 268 insertions(+), 29 deletions(-) create mode 100644 tests/Feature/Studio/Master/Outlet/CreateTest.php create mode 100644 tests/Feature/Studio/Master/Outlet/EditTest.php create mode 100644 tests/Feature/Studio/Master/Outlet/IndexTest.php diff --git a/app/Livewire/Studio/Master/Outlet/Index.php b/app/Livewire/Studio/Master/Outlet/Index.php index 5e5d517..93ff658 100644 --- a/app/Livewire/Studio/Master/Outlet/Index.php +++ b/app/Livewire/Studio/Master/Outlet/Index.php @@ -11,7 +11,6 @@ use App\Traits\Media\WithMediaHandler; use App\Traits\Utilities\WithRequestNotification; use Flux\Flux; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Str; use Illuminate\View\View; use Livewire\Attributes\Title; @@ -22,26 +21,10 @@ class Index extends Component { use WithAuthorization, WithCloseModal, WithConfirmation, WithMediaHandler, WithRequestNotification, WithToast; - public Collection $outlets; - public string $search = ''; public array $status = []; - public function mount(): void - { - $this->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(); - } - protected function formatOpeningHours(Outlet $outlet): array { if (! $outlet->openingHours || $outlet->openingHours->isEmpty()) { @@ -73,31 +56,26 @@ protected function getGalleryImages(Outlet $outlet): array ->toArray(); } - public function updatedSearch(string $value): void - { - $this->search = $value; - $this->loadOutlets(); - } - - public function updatedStatus(): void - { - $this->loadOutlets(); - } - public function delete(Outlet $outlet): void { $this->canOrAbort('delete outlet'); $outlet->delete(); $this->toast('Outlet berhasil dihapus.'); - $this->loadOutlets(); Flux::modals()->close(); } public function render(): View { + $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(); + return view('livewire.studio.master.outlet.index', [ 'pageTitle' => 'Outlet', + 'outlets' => $outlets, ]); } } diff --git a/tests/Feature/Studio/Master/Outlet/CreateTest.php b/tests/Feature/Studio/Master/Outlet/CreateTest.php new file mode 100644 index 0000000..16dd383 --- /dev/null +++ b/tests/Feature/Studio/Master/Outlet/CreateTest.php @@ -0,0 +1,96 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'create outlet']); + $role->givePermissionTo('create outlet'); + $this->user->assignRole($role); + + Storage::fake('public'); +}); + +it('renders the create outlet page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.outlet.create')) + ->assertOk() + ->assertSeeLivewire(Create::class); +}); + +it('validates required fields', function () { + Livewire::actingAs($this->user) + ->test(Create::class) + ->call('save') + ->assertHasErrors([ + 'form.name' => 'required', + 'form.phone_number' => 'required', + 'form.address' => 'required', + 'form.landmark' => 'required', + 'form.opened_date' => 'required', + 'form.featured_image' => 'required', + 'form.images' => 'required', + ]); +}); + +it('can store a new outlet with opening hours and facilities', function () { + $featuredImage = UploadedFile::fake()->image('featured.jpg'); + $galleryImage = UploadedFile::fake()->image('gallery.jpg'); + + // Prepare opening hours data + $openingHours = []; + foreach (Day::cases() as $day) { + $openingHours[$day->value] = [ + 'open_time' => '08:00', + 'close_time' => '22:00', + ]; + } + + Livewire::actingAs($this->user) + ->test(Create::class) + ->set('form.name', 'Outlet Baru') + ->set('form.phone_number', '0812 3456 7890') + ->set('form.address', 'Jl. Test No. 123') + ->set('form.landmark', 'Dekat Monas') + ->set('form.maps_url', 'https://goo.gl/maps/abcde') + ->set('form.opened_date', '2024-01-01') + ->set('form.status', OutletStatus::OPERATIONAL->value) + ->set('form.facilities', ['Wifi', 'Parkir']) + ->set('form.opening_hours', $openingHours) + // Mocking the data structure expected by handleMediaUpload/uploadMedia + ->set('form.featured_image', [['path' => $featuredImage->getPathname()]]) + ->set('form.images', [['path' => $galleryImage->getPathname()]]) + ->call('save') + ->assertHasNoErrors() + ->assertRedirect(route('studio.master.outlet.index', ['notification' => 'Outlet berhasil ditambahkan.'])); + + $this->assertDatabaseHas('outlets', [ + 'name' => 'Outlet Baru', + 'phone_number' => '0812 3456 7890', + ]); + + $outlet = Outlet::where('name', 'Outlet Baru')->first(); + expect($outlet->openingHours)->toHaveCount(7); + expect($outlet->facilities)->toHaveCount(2); +}); + +it('cannot create outlet without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + Livewire::actingAs($this->user) + ->test(Create::class) + ->set('form.name', 'Outlet Unauthorized') + ->call('save') + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/Outlet/EditTest.php b/tests/Feature/Studio/Master/Outlet/EditTest.php new file mode 100644 index 0000000..ef57bad --- /dev/null +++ b/tests/Feature/Studio/Master/Outlet/EditTest.php @@ -0,0 +1,60 @@ +setupUser(); + $this->outlet = $this->createFullOutlet(['name' => 'Original Outlet']); + $this->user->outlets()->sync([$this->outlet->id]); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'update outlet']); + $role->givePermissionTo('update outlet'); + $this->user->assignRole($role); +}); + +it('renders the edit outlet page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.outlet.edit', $this->outlet)) + ->assertOk() + ->assertSeeLivewire(Edit::class); +}); + +it('loads existing outlet data correctly', function () { + Livewire::actingAs($this->user) + ->test(Edit::class, ['outlet' => $this->outlet]) + ->assertSet('form.name', $this->outlet->name) + ->assertSet('form.phone_number', $this->outlet->phone_number); +}); + +it('can update an outlet', function () { + Livewire::actingAs($this->user) + ->test(Edit::class, ['outlet' => $this->outlet]) + ->set('form.name', 'Outlet Diperbarui') + ->set('form.phone_number', '0812 3456 7890') + ->set('form.status', OutletStatus::UNDER_CONSTRUCTION->value) + ->call('save') + ->assertHasNoErrors() + ->assertRedirect(route('studio.master.outlet.index', ['notification' => 'Outlet berhasil diperbarui.'])); + + $this->assertDatabaseHas('outlets', [ + 'id' => $this->outlet->id, + 'name' => 'Outlet Diperbarui', + 'status' => OutletStatus::UNDER_CONSTRUCTION->value, + ]); +}); + +it('cannot update outlet without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + Livewire::actingAs($this->user) + ->test(Edit::class, ['outlet' => $this->outlet]) + ->set('form.name', 'Unauthorized Update') + ->call('save') + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/Outlet/IndexTest.php b/tests/Feature/Studio/Master/Outlet/IndexTest.php new file mode 100644 index 0000000..069d66e --- /dev/null +++ b/tests/Feature/Studio/Master/Outlet/IndexTest.php @@ -0,0 +1,78 @@ +setupUser(); + + // Setup role and permission + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view outlet']); + Permission::firstOrCreate(['name' => 'delete outlet']); + $role->givePermissionTo(['view outlet', 'delete outlet']); + $this->user->assignRole($role); +}); + +it('renders the outlet index page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.outlet.index')) + ->assertOk() + ->assertSeeLivewire(Index::class); +}); + +it('displays list of outlets correctly', function () { + $outlets = Outlet::factory()->count(3)->create(); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->assertViewHas('outlets', function ($viewOutlets) { + return $viewOutlets->count() >= 3; + }) + ->assertSee($outlets->first()->name) + ->assertSee($outlets->last()->name); +}); + +it('can search outlets by name', function () { + $targetOutlet = Outlet::factory()->create(['name' => 'Outlet Spesifik']); + $otherOutlet = Outlet::factory()->create(['name' => 'Toko Biasa']); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->set('search', 'Spesifik') + ->assertSee($targetOutlet->name) + ->assertDontSee($otherOutlet->name); +}); + +it('can filter outlets by status', function () { + $operationalOutlet = Outlet::factory()->operational()->create(['name' => 'Outlet Aktif']); + $terminatedOutlet = Outlet::factory()->terminated()->create(['name' => 'Outlet Tutup']); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->set('status', [\App\Enums\OutletStatus::OPERATIONAL->value]) + ->assertSee($operationalOutlet->name) + ->assertDontSee($terminatedOutlet->name); +}); + +it('can delete an outlet', function () { + $outletToDelete = Outlet::factory()->create(); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('delete', $outletToDelete->id); + + $this->assertSoftDeleted('outlets', ['id' => $outletToDelete->id]); +}); + +it('cannot access index page without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + $this->actingAs($this->user) + ->get(route('studio.master.outlet.index')) + ->assertForbidden(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 0055774..d5b2c0e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -60,6 +60,33 @@ public function setupUser(array $overrides = []): void ->for($this->outlet) ->create(); } + + public function createFullOutlet(array $overrides = []): \App\Models\Outlet + { + $outlet = \App\Models\Outlet::factory()->create(array_merge([ + 'landmark' => 'Dekat Patung', + 'maps_url' => 'https://goo.gl/maps/test', + ], $overrides)); + + foreach (\App\Enums\Day::cases() as $day) { + \App\Models\OpeningHour::factory()->create([ + 'outlet_id' => $outlet->id, + 'day' => $day->value, + 'open_time' => '08:00', + 'close_time' => '22:00', + ]); + } + + $outlet->facilities()->create(['name' => 'Wifi']); + + $featured = \Illuminate\Http\UploadedFile::fake()->image('featured.jpg'); + $outlet->addMedia($featured->getPathname())->preservingOriginal()->toMediaCollection('featured_image'); + + $gallery = \Illuminate\Http\UploadedFile::fake()->image('gallery.jpg'); + $outlet->addMedia($gallery->getPathname())->preservingOriginal()->toMediaCollection('images'); + + return $outlet; + } } pest()->use(HasUserSetup::class)->in('Feature');