diff --git a/app/Livewire/Forms/Studio/Catalog/CategoryForm.php b/app/Livewire/Forms/Studio/Catalog/CategoryForm.php index b87c75f..d339e21 100644 --- a/app/Livewire/Forms/Studio/Catalog/CategoryForm.php +++ b/app/Livewire/Forms/Studio/Catalog/CategoryForm.php @@ -18,7 +18,7 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:20', Rule::unique('categories', 'name')->whereNull('deleted_at')->ignore($this->category)], - 'description' => ['nullable', 'string'], + 'description' => 'nullable', ]; } diff --git a/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php b/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php index ec22577..5c5d7e3 100644 --- a/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php +++ b/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php @@ -2,69 +2,567 @@ use App\Livewire\Studio\Catalog\Category; use App\Models\Category as CategoryModel; +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(Category::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 mountCategoryComponent(User $user) +{ + return Livewire::actingAs($user)->test(Category::class); +} + +/* +|-------------------------------------------------------------------------- +| Component Rendering & Mount +|-------------------------------------------------------------------------- +*/ + +it('renders page successfully', function () { + mountCategoryComponent($this->user) ->assertViewIs('livewire.studio.catalog.categories') ->assertViewHas('pageTitle', 'Kategori'); }); +it('initializes form properties on mount', function () { + $component = mountCategoryComponent($this->user); + + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe(''); + expect($component->form->name)->toBe(''); + expect($component->form->description)->toBeNull(); + expect($component->form->category)->toBeNull(); +}); + it('displays all categories', function () { CategoryModel::factory()->count(10)->create(); $category = CategoryModel::first(); - Livewire::test(Category::class)->assertSee($category->name); + mountCategoryComponent($this->user) + ->assertSee($category->name); }); -it('can create a category', function () { - $data = CategoryModel::factory()->raw(); +/* +|-------------------------------------------------------------------------- +| Open Modal Tests +|-------------------------------------------------------------------------- +*/ - Livewire::test(Category::class) +it('opens modal for create mode', function () { + $component = mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori'); + + expect($component->method)->toBe('create'); + expect($component->modalTitle)->toBe('Tambah Kategori'); + expect($component->form->category)->toBeNull(); +}); + +it('opens modal for update mode with category data', function () { + $category = CategoryModel::factory()->create([ + 'name' => 'Test Category', + 'description' => 'Test Description', + ]); + + $component = mountCategoryComponent($this->user) + ->call('openModal', 'update', 'Edit Kategori', $category->hash); + + expect($component->method)->toBe('update'); + expect($component->modalTitle)->toBe('Edit Kategori'); + expect($component->form->category->id)->toBe($category->id); + expect($component->form->name)->toBe('Test Category'); + expect($component->form->description)->toBe('Test Description'); +}); + +it('resets validation and error bag when opening modal', function () { + $component = mountCategoryComponent($this->user) + ->set('form.name', '') + ->call('create') + ->assertHasErrors(['form.name']); + + $component->call('openModal', 'create', 'Tambah Kategori') + ->assertHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| Create Category Tests +|-------------------------------------------------------------------------- +*/ + +it('prevents create when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('create category'); + Gate::define('create category', fn () => false); + + mountCategoryComponent($this->user) + ->call('create') + ->assertForbidden(); +}); + +it('creates category successfully when authorized', function () { + $data = [ + 'name' => 'New Category', + 'description' => 'Category description', + ]; + + mountCategoryComponent($this->user) ->call('openModal', 'create', 'Tambah Kategori') ->set('form.name', $data['name']) - ->set('form.slug', $data['slug']) ->set('form.description', $data['description']) - ->set('form.sort_order', $data['sort_order']) ->call('create') ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); expect(CategoryModel::count())->toBe(1); - expect(CategoryModel::first()->name)->toBe($data['name']); + $category = CategoryModel::first(); + expect($category->name)->toBe($data['name']); + expect($category->description)->toBe($data['description']); + expect($category->sort_order)->toBe(1); }); -it('can update a category', function () { +it('creates category with auto-incremented sort_order', function () { + CategoryModel::factory()->count(3)->create(); + + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Fourth Category') + ->call('create') + ->assertHasNoErrors(); + + $newCategory = CategoryModel::where('name', 'Fourth Category')->first(); + expect($newCategory->sort_order)->toBe(4); +}); + +it('creates category with null description', function () { + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Test Category') + ->set('form.description', null) + ->call('create') + ->assertHasNoErrors(); + + $category = CategoryModel::first(); + expect($category->name)->toBe('Test Category'); + expect($category->description)->toBeNull(); +}); + +/* +|-------------------------------------------------------------------------- +| Update Category Tests +|-------------------------------------------------------------------------- +*/ + +it('prevents update when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('update category'); + $category = CategoryModel::factory()->create(); - $data = CategoryModel::factory()->raw(); + Gate::define('update category', fn () => false); - Livewire::test(Category::class) - ->call('openModal', 'update', 'Edit Kategori', $category->id) - ->set('form.name', $data['name']) - ->set('form.description', $data['description']) - ->set('form.sort_order', $data['sort_order']) + mountCategoryComponent($this->user) + ->call('openModal', 'update', 'Edit Kategori', $category->hash) + ->set('form.name', 'Updated Name') + ->call('update') + ->assertForbidden(); +}); + +it('updates category successfully when authorized', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category = CategoryModel::factory()->create([ + 'name' => 'Original Name', + 'description' => 'Original Description', + ]); + + mountCategoryComponent($this->user) + ->call('openModal', 'update', 'Edit Kategori', $category->hash) + ->set('form.name', 'Updated Name') + ->set('form.description', 'Updated Description') ->call('update') ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); $category->refresh(); - - expect($category->name)->toBe($data['name']); - expect($category->description)->toBe($data['description']); + expect($category->name)->toBe('Updated Name'); + expect($category->description)->toBe('Updated Description'); }); -it('can delete an category', function () { - $category = CategoryModel::factory()->create(); +it('updates category without changing sort_order', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); - Livewire::test(Category::class) + $category = CategoryModel::factory()->create([ + 'name' => 'Original Name', + 'sort_order' => 5, + ]); + + $originalSortOrder = $category->sort_order; + + mountCategoryComponent($this->user) + ->call('openModal', 'update', 'Edit Kategori', $category->hash) + ->set('form.name', 'Updated Name') + ->call('update') + ->assertHasNoErrors(); + + $category->refresh(); + expect($category->name)->toBe('Updated Name'); + expect($category->sort_order)->toBe($originalSortOrder); +}); + +it('updates category description to null', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category = CategoryModel::factory()->create([ + 'name' => 'Test Category', + 'description' => 'Has Description', + ]); + + mountCategoryComponent($this->user) + ->call('openModal', 'update', 'Edit Kategori', $category->hash) + ->set('form.description', null) + ->call('update') + ->assertHasNoErrors(); + + $category->refresh(); + expect($category->description)->toBeNull(); +}); + +/* +|-------------------------------------------------------------------------- +| Delete Category Tests +|-------------------------------------------------------------------------- +*/ + +it('deletes category successfully', function () { + $category = CategoryModel::factory()->create([ + 'name' => 'Category To Delete', + ]); + + mountCategoryComponent($this->user) ->call('delete', $category) ->assertHasNoErrors() ->assertDispatched('refreshDatatable'); expect(CategoryModel::count())->toBe(0); + $this->assertSoftDeleted('categories', [ + 'id' => $category->id, + ]); +}); + +it('adjusts sort_order of remaining categories after deletion', function () { + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $category3 = CategoryModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + + mountCategoryComponent($this->user) + ->call('delete', $category2); + + $category1->refresh(); + $category3->refresh(); + + expect($category1->sort_order)->toBe(1); + expect($category3->sort_order)->toBe(2); +}); + +it('does not adjust sort_order when deleting last category', function () { + $category = CategoryModel::factory()->create(['sort_order' => 1]); + + mountCategoryComponent($this->user) + ->call('delete', $category); + + expect(CategoryModel::count())->toBe(0); +}); + +/* +|-------------------------------------------------------------------------- +| Sort Order Tests +|-------------------------------------------------------------------------- +*/ + +it('prevents sortOrder when user is unauthorized', function () { + $this->ownerRole->revokePermissionTo('update category'); + + $category = CategoryModel::factory()->create(['sort_order' => 2]); + + Gate::define('update category', fn () => false); + + mountCategoryComponent($this->user) + ->call('sortOrder', $category, 'up') + ->assertForbidden(); +}); + +it('moves category up when authorized', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + mountCategoryComponent($this->user) + ->call('sortOrder', $category2, 'up') + ->assertDispatched('refreshDatatable'); + + $category1->refresh(); + $category2->refresh(); + + expect($category1->sort_order)->toBe(2); + expect($category2->sort_order)->toBe(1); +}); + +it('moves category down when authorized', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + mountCategoryComponent($this->user) + ->call('sortOrder', $category1, 'down') + ->assertDispatched('refreshDatatable'); + + $category1->refresh(); + $category2->refresh(); + + expect($category1->sort_order)->toBe(2); + expect($category2->sort_order)->toBe(1); +}); + +it('does not move category up when it is already first', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + $originalOrder1 = $category1->sort_order; + $originalOrder2 = $category2->sort_order; + + mountCategoryComponent($this->user) + ->call('sortOrder', $category1, 'up'); + + $category1->refresh(); + $category2->refresh(); + + expect($category1->sort_order)->toBe($originalOrder1); + expect($category2->sort_order)->toBe($originalOrder2); +}); + +it('does not move category down when it is already last', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + + $originalOrder1 = $category1->sort_order; + $originalOrder2 = $category2->sort_order; + + mountCategoryComponent($this->user) + ->call('sortOrder', $category2, 'down'); + + $category1->refresh(); + $category2->refresh(); + + expect($category1->sort_order)->toBe($originalOrder1); + expect($category2->sort_order)->toBe($originalOrder2); +}); + +it('moves category to first position', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $category3 = CategoryModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + + mountCategoryComponent($this->user) + ->call('sortOrder', $category3, 'first') + ->assertDispatched('refreshDatatable'); + + $category1->refresh(); + $category2->refresh(); + $category3->refresh(); + + expect($category3->sort_order)->toBe(1); + expect($category1->sort_order)->toBe(2); + expect($category2->sort_order)->toBe(3); +}); + +it('moves category to last position', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']); + $category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']); + $category3 = CategoryModel::factory()->create(['sort_order' => 3, 'name' => 'Third']); + + mountCategoryComponent($this->user) + ->call('sortOrder', $category1, 'last') + ->assertDispatched('refreshDatatable'); + + $category1->refresh(); + $category2->refresh(); + $category3->refresh(); + + expect($category1->sort_order)->toBe(3); + expect($category2->sort_order)->toBe(1); + expect($category3->sort_order)->toBe(2); +}); + +/* +|-------------------------------------------------------------------------- +| Validation Tests +|-------------------------------------------------------------------------- +*/ + +it('validates required name field on create', function () { + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', '') + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('validates name field max length', function () { + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', str_repeat('a', 21)) + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('validates unique name on create', function () { + $existingCategory = CategoryModel::factory()->create(['name' => 'Existing Category']); + + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Existing Category') + ->call('create') + ->assertHasErrors(['form.name']); +}); + +it('allows same name on update of same category', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category = CategoryModel::factory()->create(['name' => 'Original Name']); + + mountCategoryComponent($this->user) + ->call('openModal', 'update', 'Edit Kategori', $category->hash) + ->set('form.name', 'Original Name') + ->call('update') + ->assertHasNoErrors(); +}); + +it('validates unique name on update with different category name', function () { + $permission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo($permission); + + $category1 = CategoryModel::factory()->create(['name' => 'Category One']); + $category2 = CategoryModel::factory()->create(['name' => 'Category Two']); + + mountCategoryComponent($this->user) + ->dispatch('modal:open', 'update', 'Edit Kategori', $category1->hash) + ->set('form.name', 'Category Two') + ->call('update') + ->assertHasErrors(['form.name']); +}); + +it('validates name uniqueness ignores soft deleted categories', function () { + $deletedCategory = CategoryModel::factory()->create(['name' => 'Deleted Category']); + $deletedCategory->delete(); + + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Deleted Category') + ->call('create') + ->assertHasNoErrors(); +}); + +it('validates description is nullable', function () { + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Test Category') + ->set('form.description', null) + ->call('create') + ->assertHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| Integration Tests +|-------------------------------------------------------------------------- +*/ + +it('creates category and generates slug automatically', function () { + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Test Category Name') + ->call('create') + ->assertHasNoErrors(); + + $category = CategoryModel::first(); + expect($category->slug)->not->toBeNull(); + expect($category->slug)->toBeString(); +}); + +it('maintains data integrity after multiple operations', function () { + $createPermission = Permission::where('name', 'create category')->first(); + $updatePermission = Permission::where('name', 'update category')->first(); + $this->user->givePermissionTo([$createPermission, $updatePermission]); + + // Create categories + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Category One') + ->call('create'); + + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Category Two') + ->call('create'); + + mountCategoryComponent($this->user) + ->call('openModal', 'create', 'Tambah Kategori') + ->set('form.name', 'Category Three') + ->call('create'); + + // Update category + $category2 = CategoryModel::where('name', 'Category Two')->first(); + mountCategoryComponent($this->user) + ->dispatch('modal:open', 'update', 'Edit Kategori', $category2->hash) + ->set('form.name', 'Category Two Updated') + ->call('update'); + + // Delete category + $category1 = CategoryModel::where('name', 'Category One')->first(); + mountCategoryComponent($this->user) + ->call('delete', $category1); + + expect(CategoryModel::count())->toBe(2); + $remainingCategories = CategoryModel::orderBy('sort_order')->get(); + expect($remainingCategories[0]->name)->toBe('Category Two Updated'); + expect($remainingCategories[0]->sort_order)->toBe(1); + expect($remainingCategories[1]->name)->toBe('Category Three'); + expect($remainingCategories[1]->sort_order)->toBe(2); });