diff --git a/app/Livewire/Forms/Studio/Catalog/BrandForm.php b/app/Livewire/Forms/Studio/Catalog/BrandForm.php index 17ba5d3..a0567c5 100644 --- a/app/Livewire/Forms/Studio/Catalog/BrandForm.php +++ b/app/Livewire/Forms/Studio/Catalog/BrandForm.php @@ -21,7 +21,7 @@ class BrandForm extends Form public function rules(): array { return [ - 'name' => ['required', 'string', 'max:20', Rule::unique('categories', 'name')->whereNull('deleted_at')->ignore($this->brand)], + 'name' => ['required', 'max:20', Rule::unique('brands', 'name')->whereNull('deleted_at')->ignore($this->brand)], 'image' => ['nullable', 'array'], ]; } diff --git a/tests/Feature/Livewire/Studio/Catalog/BrandTest.php b/tests/Feature/Livewire/Studio/Catalog/BrandTest.php index 2d9ad9b..4d8d79e 100644 --- a/tests/Feature/Livewire/Studio/Catalog/BrandTest.php +++ b/tests/Feature/Livewire/Studio/Catalog/BrandTest.php @@ -2,66 +2,572 @@ use App\Livewire\Studio\Catalog\Brand; use App\Models\Brand as BrandModel; +use App\Models\Employee; +use App\Models\User; +use Database\Seeders\RolePermissionSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; use Livewire\Livewire; +use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; uses(RefreshDatabase::class); -it('renders successfully', function () { - Livewire::test(Brand::class) +beforeEach(function () { + $this->seed(RolePermissionSeeder::class); + + $this->user = User::factory() + ->active() + ->has(Employee::factory()) + ->create(); + + $this->ownerRole = Role::where('name', 'Owner')->first(); + $this->user->roles()->attach($this->ownerRole->id); +}); + +function mountBrandComponent(User $user) +{ + return Livewire::actingAs($user)->test(Brand::class); +} + +/* +|-------------------------------------------------------------------------- +| Component Rendering & Mount +|-------------------------------------------------------------------------- +*/ + +it('renders page successfully', function () { + mountBrandComponent($this->user) ->assertViewIs('livewire.studio.catalog.brands') ->assertViewHas('pageTitle', 'Merek'); }); +it('initializes form properties on mount', function () { + $component = mountBrandComponent($this->user); + + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe(''); + expect($component->form->name)->toBe(''); + expect($component->form->image)->toBe([]); + expect($component->form->brand)->toBeNull(); +}); + it('displays all brands', function () { BrandModel::factory()->count(10)->create(); $brand = BrandModel::first(); - Livewire::test(Brand::class)->assertSee($brand->name); + mountBrandComponent($this->user) + ->assertSee($brand->name); }); -it('can create a brand', function () { - $data = BrandModel::factory()->raw(); +/* +|-------------------------------------------------------------------------- +| Open Modal Tests +|-------------------------------------------------------------------------- +*/ - Livewire::test(Brand::class) +it('opens modal for create mode', function () { + $component = mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek'); + + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe('Tambah Merek'); + expect($component->form->brand)->toBeNull(); +}); + +it('opens modal for update mode with brand data', function () { + $brand = BrandModel::factory()->create([ + 'name' => 'Test Brand', + ]); + + $component = mountBrandComponent($this->user) + ->call('openModal', 'update', 'Edit Merek', $brand->hash); + + expect($component->method)->toBe('update'); + expect($component->modalTitle)->toBe('Edit Merek'); + expect($component->form->brand->id)->toBe($brand->id); + expect($component->form->name)->toBe('Test Brand'); +}); + +it('resets validation and error bag when opening modal', function () { + $component = mountBrandComponent($this->user) + ->set('form.name', '') + ->call('create') + ->assertHasErrors(['form.name']); + + $component->call('openModal', 'create', 'Tambah Merek') + ->assertHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| Create Brand Tests +|-------------------------------------------------------------------------- +*/ + +it('prevents create when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('create brand'); + Gate::define('create brand', fn () => false); + + mountBrandComponent($this->user) + ->call('create') + ->assertForbidden(); +}); + +it('creates brand successfully when authorized', function () { + $data = [ + 'name' => 'New Brand', + ]; + + mountBrandComponent($this->user) ->call('openModal', 'create', 'Tambah Merek') ->set('form.name', $data['name']) - ->set('form.slug', $data['slug']) - ->set('form.sort_order', $data['sort_order']) ->call('create') ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); expect(BrandModel::count())->toBe(1); - expect(BrandModel::first()->name)->toBe($data['name']); + $brand = BrandModel::first(); + expect($brand->name)->toBe($data['name']); + expect($brand->sort_order)->toBe(1); }); -it('can update a brand', function () { +it('creates brand with auto-incremented sort_order', function () { + BrandModel::factory()->count(3)->create(); + + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Fourth Brand') + ->call('create') + ->assertHasNoErrors(); + + $newBrand = BrandModel::where('name', 'Fourth Brand')->first(); + expect($newBrand->sort_order)->toBe(4); +}); + +it('creates brand with empty image array', function () { + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Brand Without Image') + ->set('form.image', []) + ->call('create') + ->assertHasNoErrors(); + + $brand = BrandModel::first(); + expect($brand->name)->toBe('Brand Without Image'); +}); + +it('creates brand and generates slug automatically', function () { + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Test Brand Name') + ->call('create') + ->assertHasNoErrors(); + + $brand = BrandModel::first(); + expect($brand->slug)->not->toBeNull(); + expect($brand->slug)->toBeString(); +}); + +/* +|-------------------------------------------------------------------------- +| Update Brand Tests +|-------------------------------------------------------------------------- +*/ + +it('prevents update when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('update brand'); + $brand = BrandModel::factory()->create(); - $data = BrandModel::factory()->raw(); + Gate::define('update brand', fn () => false); - Livewire::test(Brand::class) - ->call('openModal', 'update', 'Edit Merek', $brand->id) - ->set('form.name', $data['name']) - ->set('form.sort_order', $data['sort_order']) + mountBrandComponent($this->user) + ->call('openModal', 'update', 'Edit Merek', $brand->hash) + ->set('form.name', 'Updated Name') + ->call('update') + ->assertForbidden(); +}); + +it('updates brand successfully when authorized', function () { + $brand = BrandModel::factory()->create([ + 'name' => 'Original Name', + ]); + + mountBrandComponent($this->user) + ->call('openModal', 'update', 'Edit Merek', $brand->hash) + ->set('form.name', 'Updated Name') ->call('update') ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); $brand->refresh(); - - expect($brand->name)->toBe($data['name']); + expect($brand->name)->toBe('Updated Name'); }); -it('can delete an brand', function () { - $brand = BrandModel::factory()->create(); +it('updates brand without changing sort_order', function () { + $brand = BrandModel::factory()->create([ + 'name' => 'Original Name', + 'sort_order' => 5, + ]); - Livewire::test(Brand::class) + $originalSortOrder = $brand->sort_order; + + mountBrandComponent($this->user) + ->call('openModal', 'update', 'Edit Merek', $brand->hash) + ->set('form.name', 'Updated Name') + ->call('update') + ->assertHasNoErrors(); + + $brand->refresh(); + expect($brand->name)->toBe('Updated Name'); + expect($brand->sort_order)->toBe($originalSortOrder); +}); + +it('updates brand with empty image array', function () { + $brand = BrandModel::factory()->create([ + 'name' => 'Test Brand', + ]); + + mountBrandComponent($this->user) + ->call('openModal', 'update', 'Edit Merek', $brand->hash) + ->set('form.image', []) + ->call('update') + ->assertHasNoErrors(); + + $brand->refresh(); + expect($brand->name)->toBe('Test Brand'); +}); + +/* +|-------------------------------------------------------------------------- +| Delete Brand Tests +|-------------------------------------------------------------------------- +*/ + +it('deletes brand successfully', function () { + $brand = BrandModel::factory()->create([ + 'name' => 'Brand To Delete', + ]); + + mountBrandComponent($this->user) ->call('delete', $brand) ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); expect(BrandModel::count())->toBe(0); + $this->assertSoftDeleted('brands', [ + 'id' => $brand->id, + ]); +}); + +it('adjusts sort_order of remaining brands after deletion', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + + mountBrandComponent($this->user) + ->call('delete', $brand2); + + $brand1->refresh(); + $brand3->refresh(); + + expect($brand1->sort_order)->toBe(1); + expect($brand3->sort_order)->toBe(2); +}); + +it('does not adjust sort_order when deleting last brand', function () { + $brand = BrandModel::factory()->create(['sort_order' => 1]); + + mountBrandComponent($this->user) + ->call('delete', $brand); + + expect(BrandModel::count())->toBe(0); +}); + +/* +|-------------------------------------------------------------------------- +| Sort Order Tests +|-------------------------------------------------------------------------- +*/ + +it('prevents sortOrder when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('update brand'); + + $brand = BrandModel::factory()->create(['sort_order' => 2]); + + Gate::define('update brand', fn () => false); + + mountBrandComponent($this->user) + ->call('sortOrder', $brand, 'up') + ->assertForbidden(); +}); + +it('moves brand up when authorized', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + mountBrandComponent($this->user) + ->call('sortOrder', $brand2, 'up') + ->assertDispatched('refreshDatatable'); + + $brand1->refresh(); + $brand2->refresh(); + + expect($brand1->sort_order)->toBe(2); + expect($brand2->sort_order)->toBe(1); +}); + +it('moves brand down when authorized', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + mountBrandComponent($this->user) + ->call('sortOrder', $brand1, 'down') + ->assertDispatched('refreshDatatable'); + + $brand1->refresh(); + $brand2->refresh(); + + expect($brand1->sort_order)->toBe(2); + expect($brand2->sort_order)->toBe(1); +}); + +it('does not move brand up when it is already first', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + $originalOrder1 = $brand1->sort_order; + $originalOrder2 = $brand2->sort_order; + + mountBrandComponent($this->user) + ->call('sortOrder', $brand1, 'up'); + + $brand1->refresh(); + $brand2->refresh(); + + expect($brand1->sort_order)->toBe($originalOrder1); + expect($brand2->sort_order)->toBe($originalOrder2); +}); + +it('does not move brand down when it is already last', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + $originalOrder1 = $brand1->sort_order; + $originalOrder2 = $brand2->sort_order; + + mountBrandComponent($this->user) + ->call('sortOrder', $brand2, 'down'); + + $brand1->refresh(); + $brand2->refresh(); + + expect($brand1->sort_order)->toBe($originalOrder1); + expect($brand2->sort_order)->toBe($originalOrder2); +}); + +it('moves brand to first position', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + + mountBrandComponent($this->user) + ->call('sortOrder', $brand3, 'first') + ->assertDispatched('refreshDatatable'); + + $brand1->refresh(); + $brand2->refresh(); + $brand3->refresh(); + + expect($brand3->sort_order)->toBe(1); + expect($brand1->sort_order)->toBe(2); + expect($brand2->sort_order)->toBe(3); +}); + +it('moves brand to last position', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + + mountBrandComponent($this->user) + ->call('sortOrder', $brand1, 'last') + ->assertDispatched('refreshDatatable'); + + $brand1->refresh(); + $brand2->refresh(); + $brand3->refresh(); + + expect($brand1->sort_order)->toBe(3); + expect($brand2->sort_order)->toBe(1); + expect($brand3->sort_order)->toBe(2); +}); + +/* +|-------------------------------------------------------------------------- +| Validation Tests +|-------------------------------------------------------------------------- +*/ + +it('validates required name field on create', function () { + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', '') + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('validates name field max length', function () { + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', str_repeat('a', 21)) + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('validates unique name on create', function () { + $existingBrand = BrandModel::factory()->create(['name' => 'Existing Brand']); + + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Existing Brand') + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('allows same name on update of same brand', function () { + $brand = BrandModel::factory()->create(['name' => 'Original Name']); + + mountBrandComponent($this->user) + ->call('openModal', 'update', 'Edit Merek', $brand->hash) + ->set('form.name', 'Original Name') + ->call('update') + ->assertHasNoErrors(); +}); + +it('validates unique name on update with different brand name', function () { + $brand1 = BrandModel::factory()->create(['name' => 'Brand One']); + $brand2 = BrandModel::factory()->create(['name' => 'Brand Two']); + + mountBrandComponent($this->user) + ->dispatch('modal:open', 'update', 'Edit Merek', $brand1->hash) + ->set('form.name', 'Brand Two') + ->call('update') + ->assertHasErrors(['form.name']); +}); + +it('validates name uniqueness ignores soft deleted brands', function () { + $deletedBrand = BrandModel::factory()->create(['name' => 'Deleted Brand']); + $deletedBrand->delete(); + + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Deleted Brand') + ->call('create') + ->assertHasNoErrors(); +}); + +it('validates image is nullable', function () { + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Test Brand') + ->set('form.image', null) + ->call('create') + ->assertHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| Integration Tests +|-------------------------------------------------------------------------- +*/ + +it('maintains data integrity after multiple operations', function () { + $createPermission = Permission::where('name', 'create brand')->first(); + $updatePermission = Permission::where('name', 'update brand')->first(); + $this->user->givePermissionTo([$createPermission, $updatePermission]); + + // Create brands + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Brand One') + ->call('create'); + + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Brand Two') + ->call('create'); + + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Brand Three') + ->call('create'); + + // Update brand + $brand2 = BrandModel::where('name', 'Brand Two')->first(); + mountBrandComponent($this->user) + ->dispatch('modal:open', 'update', 'Edit Merek', $brand2->hash) + ->set('form.name', 'Brand Two Updated') + ->call('update'); + + // Delete brand + $brand1 = BrandModel::where('name', 'Brand One')->first(); + mountBrandComponent($this->user) + ->call('delete', $brand1); + + expect(BrandModel::count())->toBe(2); + $remainingBrands = BrandModel::orderBy('sort_order')->get(); + expect($remainingBrands[0]->name)->toBe('Brand Two Updated'); + expect($remainingBrands[0]->sort_order)->toBe(1); + expect($remainingBrands[1]->name)->toBe('Brand Three'); + expect($remainingBrands[1]->sort_order)->toBe(2); +}); + +it('handles sort order operations correctly with multiple brands', function () { + $brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + $brand4 = BrandModel::factory()->create(['sort_order' => 4, 'name' => 'Fourth']); + + // Move brand3 to first + mountBrandComponent($this->user) + ->call('sortOrder', $brand3, 'first'); + + $brand1->refresh(); + $brand2->refresh(); + $brand3->refresh(); + $brand4->refresh(); + + expect($brand3->sort_order)->toBe(1); + expect($brand1->sort_order)->toBe(2); + expect($brand2->sort_order)->toBe(3); + expect($brand4->sort_order)->toBe(4); + + // Move brand2 to last + mountBrandComponent($this->user) + ->call('sortOrder', $brand2, 'last'); + + $brand1->refresh(); + $brand2->refresh(); + $brand3->refresh(); + $brand4->refresh(); + + expect($brand3->sort_order)->toBe(1); + expect($brand1->sort_order)->toBe(2); + expect($brand4->sort_order)->toBe(3); + expect($brand2->sort_order)->toBe(4); +}); + +it('creates brand with correct slug generation', function () { + mountBrandComponent($this->user) + ->call('openModal', 'create', 'Tambah Merek') + ->set('form.name', 'Yves Saint Laurent') + ->call('create') + ->assertHasNoErrors(); + + $brand = BrandModel::first(); + expect($brand->slug)->not->toBeNull(); + expect($brand->slug)->toBeString(); + expect(strlen($brand->slug))->toBeLessThanOrEqual(30); });