user = User::factory() ->active() ->has(Employee::factory()) ->create(); }); function mountCreateComponent(User $user) { return Livewire::actingAs($user)->test(Create::class); } it('renders page successfully', function () { mountCreateComponent($this->user) ->assertViewIs('livewire.studio.master.outlet.form') ->assertViewHas('pageTitle', 'Tambah Outlet'); }); it('mounts days and form opening_hours correctly', function () { $component = mountCreateComponent($this->user); $days = Day::cases(); expect($component->days)->toHaveCount(count($days)); foreach ($days as $day) { expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue(); expect($component->form->opening_hours[$day->value])->toBe(['open_time' => null, 'close_time' => null]); } }); it('prevents create when user is unauthorized', function () { Gate::define('create outlet', fn () => false); mountCreateComponent($this->user) ->call('save') ->assertForbidden(); }); it('saves outlet successfully when authorized', function () { $permission = Permission::create(['name' => 'create outlet']); $this->user->givePermissionTo($permission); $outlet = Outlet::factory()->operational()->raw(); $facilities = Facility::factory()->count(5)->raw(); $featuredImage = makeLivewireFile('featured.jpg'); $images = []; for ($i = 1; $i <= 5; $i++) { $images[] = makeLivewireFile("image{$i}.jpg"); } $openingHours = collect(Day::cases()) ->mapWithKeys(fn ($day) => [ $day->value => [ 'day' => $day->value, 'open_time' => fake()->time('H:i'), 'close_time' => fake()->time('H:i'), ], ]) ->toArray(); $data = array_merge($outlet, [ 'opening_hours' => $openingHours, 'facilities' => collect($facilities)->pluck('name')->toArray(), 'featured_image' => [$featuredImage], 'images' => $images, ]); mountCreateComponent($this->user) ->set('form.name', $data['name']) ->set('form.phone_number', $data['phone_number']) ->set('form.address', $data['address']) ->set('form.landmark', $data['landmark']) ->set('form.maps_url', $data['maps_url']) ->set('form.opened_date', $data['opened_date']) ->set('form.status', $data['status']->value) ->set('form.opening_hours', $data['opening_hours']) ->set('form.facilities', $data['facilities']) ->set('form.featured_image', $data['featured_image']) ->set('form.images', $data['images']) ->call('save') ->assertHasNoErrors() ->assertRedirect(Index::class); $outlet = Outlet::first(); $this->assertDatabaseHas('outlets', [ 'id' => $outlet->id, 'name' => $data['name'], 'phone_number' => $data['phone_number'], ]); $this->assertCount(1, $outlet->getMedia('featured_image')); $this->assertCount(5, $outlet->getMedia('images')); expect($outlet->getFirstMedia('featured_image')->getPath())->toBeFile(); foreach ($outlet->getMedia('images') as $media) { expect($media->getPath())->toBeFile(); } }); it('validates required fields', function () { $permission = Permission::create(['name' => 'create outlet']); $this->user->givePermissionTo($permission); mountCreateComponent($this->user) ->call('save') ->assertHasErrors([ 'form.name', 'form.phone_number', 'form.address', 'form.landmark', 'form.opened_date', 'form.featured_image', 'form.images', ]); }); it('validates name field max length', function () { $permission = Permission::create(['name' => 'create outlet']); $this->user->givePermissionTo($permission); mountCreateComponent($this->user) ->set('form.name', str_repeat('a', 101)) ->call('save') ->assertHasErrors(['form.name']); }); it('validates facilities minimum count', function () { $permission = Permission::create(['name' => 'create outlet']); $this->user->givePermissionTo($permission); mountCreateComponent($this->user) ->set('form.facilities', []) ->call('save') ->assertHasErrors(['form.facilities']); }); it('validates featured image max count', function () { $permission = Permission::create(['name' => 'create outlet']); $this->user->givePermissionTo($permission); $featuredImages = [ makeLivewireFile('featured1.jpg'), makeLivewireFile('featured2.jpg'), ]; mountCreateComponent($this->user) ->set('form.featured_image', $featuredImages) ->call('save') ->assertHasErrors(['form.featured_image']); }); it('validates images max count', function () { $permission = Permission::create(['name' => 'create outlet']); $this->user->givePermissionTo($permission); $images = []; for ($i = 1; $i <= 6; $i++) { $images[] = makeLivewireFile("image{$i}.jpg"); } mountCreateComponent($this->user) ->set('form.images', $images) ->call('save') ->assertHasErrors(['form.images']); }); it('has default facilities value', function () { $component = mountCreateComponent($this->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); $outlet = Outlet::factory()->operational()->raw(); $featuredImage = makeLivewireFile('featured.jpg'); $images = [makeLivewireFile('image1.jpg')]; $openingHours = collect(Day::cases()) ->mapWithKeys(fn ($day) => [ $day->value => [ 'open_time' => '09:00', 'close_time' => '21:00', ], ]) ->toArray(); 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.opening_hours', $openingHours) ->set('form.facilities', ['Parkir']) ->set('form.featured_image', [$featuredImage]) ->set('form.images', $images) ->call('save') ->assertHasNoErrors(); $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(); });