feat(brand): implementasi testing dan fix table untuk validasi unik

This commit is contained in:
Yoga Pangestu 2025-12-08 13:42:47 +07:00
parent c46702a490
commit d27195a053
2 changed files with 527 additions and 21 deletions

View File

@ -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'],
];
}

View File

@ -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);
});