From 598ca3500fe52473c52da0698f689f2b88468d52 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 2 Jan 2026 18:18:13 +0700 Subject: [PATCH] feat: Add comprehensive feature tests for master data modules and refine Livewire forms. --- .../Forms/Studio/Master/FormulaForm.php | 12 +- .../Forms/Studio/Master/WarehouseForm.php | 2 +- app/Livewire/Studio/Master/Formula.php | 100 +++---------- app/Livewire/Studio/Master/User/Index.php | 11 +- tests/Feature/Studio/Master/CategoryTest.php | 129 ++++++++++++++++ tests/Feature/Studio/Master/FormulaTest.php | 139 ++++++++++++++++++ .../Feature/Studio/Master/User/IndexTest.php | 2 +- .../Studio/Master/Warehouse/IndexTest.php | 107 ++++++++++++++ .../Studio/Master/Warehouse/ShowTest.php | 63 ++++++++ 9 files changed, 470 insertions(+), 95 deletions(-) create mode 100644 tests/Feature/Studio/Master/CategoryTest.php create mode 100644 tests/Feature/Studio/Master/FormulaTest.php create mode 100644 tests/Feature/Studio/Master/Warehouse/IndexTest.php create mode 100644 tests/Feature/Studio/Master/Warehouse/ShowTest.php diff --git a/app/Livewire/Forms/Studio/Master/FormulaForm.php b/app/Livewire/Forms/Studio/Master/FormulaForm.php index daa8f52..e5036a9 100644 --- a/app/Livewire/Forms/Studio/Master/FormulaForm.php +++ b/app/Livewire/Forms/Studio/Master/FormulaForm.php @@ -43,23 +43,23 @@ public function setFormula(Formula $formula): void $this->volume = parseRupiahToInt($formula->volume); } - public function store(): Formula + public function store(): void { $this->validate(); - return DB::transaction(function () { - return Formula::create($this->prepareDataForSave()); + DB::transaction(function () { + Formula::create($this->prepareDataForSave()); }); } - public function update(): Formula + public function update(): void { $this->validate(); - return DB::transaction(function () { + DB::transaction(function () { $this->formula->update($this->prepareDataForSave()); - return $this->formula; + $this->formula; }); } diff --git a/app/Livewire/Forms/Studio/Master/WarehouseForm.php b/app/Livewire/Forms/Studio/Master/WarehouseForm.php index 5909af5..745337a 100644 --- a/app/Livewire/Forms/Studio/Master/WarehouseForm.php +++ b/app/Livewire/Forms/Studio/Master/WarehouseForm.php @@ -7,7 +7,7 @@ class WarehouseForm extends Form { - public ?Warehouse $warehouse; + public ?Warehouse $warehouse = null; public string $name = ''; diff --git a/app/Livewire/Studio/Master/Formula.php b/app/Livewire/Studio/Master/Formula.php index 2cb5c7c..aa74c3d 100644 --- a/app/Livewire/Studio/Master/Formula.php +++ b/app/Livewire/Studio/Master/Formula.php @@ -24,8 +24,6 @@ class Formula extends Component public FormulaForm $form; - public array $formulas = []; - public string $method = 'create'; public string $modalTitle = ''; @@ -36,22 +34,6 @@ class Formula extends Component public function mount(): void { - $this->formulas = FormulaModel::orderBy('size') - ->get() - ->groupBy('size') - ->map( - fn ($items) => $items - ->map(fn ($item) => [ - 'hash' => $item->hash, - 'quality' => $item->quality, - 'volume' => $item->volume, - ]) - ->sortBy('volume') - ->values() - ->toArray() - ) - ->toArray(); - $this->sizes = Bottle::distinct()->orderBy('size')->pluck('size')->toArray(); } @@ -65,30 +47,7 @@ public function create(): void return; } - $formula = $this->form->store(); - - $size = $formula->size; - - $newFormula = [ - 'hash' => $formula->hash, - 'quality' => $formula->quality, - 'volume' => $formula->volume, - ]; - - // Convert to collection for manipulation - $formulas = collect($this->formulas); - - // Get the group for this size, or an empty collection - $group = collect($formulas->get($size, [])); - - // Add, then re-sort - $group = $group->push($newFormula) - ->sortBy('volume') - ->values() - ->toArray(); - - // Put it back and convert to array - $this->formulas = $formulas->put($size, $group)->toArray(); + $this->form->store(); $this->toast('Rumus berhasil ditambahkan.'); @@ -110,25 +69,7 @@ public function update(): void return; } - $formula = $this->form->update(); - - // Convert to collection for manipulation - $this->formulas = collect($this->formulas) - ->map(function ($group) use ($formula) { - return collect($group) - ->map(function ($item) use ($formula) { - if ($item['hash'] === $formula->hash) { - $item['quality'] = $formula->quality; - $item['volume'] = $formula->volume; - } - - return $item; - }) - ->sortBy('volume') - ->values() - ->toArray(); - }) - ->toArray(); + $this->form->update(); $this->toast('Rumus berhasil diperbarui.'); @@ -141,26 +82,6 @@ public function delete(FormulaModel $formula): void $formula->delete(); - // Convert to collection for manipulation - $size = $formula->size; - $formulas = collect($this->formulas); - - if ($formulas->has($size)) { - $group = collect($formulas->get($size)) - ->reject(fn ($item) => $item['hash'] === $formula->hash) - ->values() - ->toArray(); - - if (empty($group)) { - $formulas->forget($size); - } else { - $formulas->put($size, $group); - } - } - - // Convert back to array - $this->formulas = $formulas->toArray(); - $this->toast('Rumus berhasil dihapus.'); Flux::modals()->close(); @@ -180,8 +101,25 @@ public function openModal(string $method, string $modalTitle, ?string $id = null public function render(): View { + $formulas = FormulaModel::orderBy('size') + ->get() + ->groupBy('size') + ->map( + fn ($items) => $items + ->map(fn ($item) => [ + 'hash' => $item->hash, + 'quality' => $item->quality, + 'volume' => $item->volume, + ]) + ->sortBy('volume') + ->values() + ->toArray() + ) + ->toArray(); + return view('livewire.studio.master.formulas', [ 'pageTitle' => 'Rumus', + 'formulas' => $formulas, ]); } } diff --git a/app/Livewire/Studio/Master/User/Index.php b/app/Livewire/Studio/Master/User/Index.php index c9d3d90..423e85f 100644 --- a/app/Livewire/Studio/Master/User/Index.php +++ b/app/Livewire/Studio/Master/User/Index.php @@ -10,7 +10,6 @@ use App\Traits\Notification\WithSubscribeNotification; use App\Traits\Utilities\WithRequestNotification; use Flux\Flux; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Hash; use Illuminate\View\View; use Livewire\Attributes\Title; @@ -51,12 +50,12 @@ public function render(): View { $employees = Employee::with(['user', 'user.referralCode', 'user.outlets']) ->when(! empty($this->search), function ($query) { - return $query->where('full_name', 'like', '%' . $this->search . '%') - ->orWhere('code', 'like', '%' . $this->search . '%') - ->orWhereHas('user', fn($q) => $q->where('email', 'like', '%' . $this->search . '%')) - ->orWhereHas('user', fn($q) => $q->where('username', 'like', '%' . $this->search . '%')); + return $query->where('full_name', 'like', '%'.$this->search.'%') + ->orWhere('code', 'like', '%'.$this->search.'%') + ->orWhereHas('user', fn ($q) => $q->where('email', 'like', '%'.$this->search.'%')) + ->orWhereHas('user', fn ($q) => $q->where('username', 'like', '%'.$this->search.'%')); }) - ->when(! empty($this->status), fn($query) => $query->whereHas('user', fn($q) => $q->whereIn('status', $this->status))) + ->when(! empty($this->status), fn ($query) => $query->whereHas('user', fn ($q) => $q->whereIn('status', $this->status))) ->withoutDeveloper() ->latest() ->get(); diff --git a/tests/Feature/Studio/Master/CategoryTest.php b/tests/Feature/Studio/Master/CategoryTest.php new file mode 100644 index 0000000..5bc6227 --- /dev/null +++ b/tests/Feature/Studio/Master/CategoryTest.php @@ -0,0 +1,129 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view category']); + Permission::firstOrCreate(['name' => 'create category']); + Permission::firstOrCreate(['name' => 'update category']); + Permission::firstOrCreate(['name' => 'delete category']); + $role->givePermissionTo(['view category', 'create category', 'update category', 'delete category']); + $this->user->assignRole($role); +}); + +it('renders the category page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.category.index')) + ->assertOk() + ->assertSeeLivewire(Category::class); +}); + +it('can open create modal', function () { + Livewire::actingAs($this->user) + ->test(Category::class) + ->dispatch('modal:open', method: 'create', modalTitle: 'Tambah Kategori') + ->assertSet('method', 'create') + ->assertSet('modalTitle', 'Tambah Kategori'); +}); + +it('validates required fields on create', function () { + Livewire::actingAs($this->user) + ->test(Category::class) + ->call('create') + ->assertHasErrors(['form.name' => 'required']); +}); + +it('can store a new category', function () { + Livewire::actingAs($this->user) + ->test(Category::class) + ->set('form.name', 'Kategori Baru') + ->set('form.type', CategoryType::PERFUME->value) + ->set('form.description', 'Deskripsi Kategori') + ->call('create') + ->assertHasNoErrors() + ->assertDispatched('refreshDatatable'); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Kategori Baru', + 'type' => CategoryType::PERFUME->value, + 'description' => 'Deskripsi Kategori', + ]); +}); + +it('can open edit modal and load data', function () { + $category = CategoryModel::factory()->create([ + 'name' => 'Kategori Edit', + 'type' => CategoryType::ARTICLE->value, + ]); + + Livewire::actingAs($this->user) + ->test(Category::class) + ->call('openModal', 'update', 'Ubah Kategori', $category->hash_id) + ->set('form.category', $category) + ->set('form.name', $category->name) + ->set('form.type', $category->type->value) + ->assertSet('method', 'update') + ->assertSet('modalTitle', 'Ubah Kategori') + ->assertSet('form.name', 'Kategori Edit') + ->assertSet('form.type', CategoryType::ARTICLE->value); +}); + +it('can update a category', function () { + $category = CategoryModel::factory()->create(['name' => 'Kategori Lama', 'type' => CategoryType::PERFUME->value]); + + Livewire::actingAs($this->user) + ->test(Category::class) + ->call('openModal', 'update', 'Ubah Kategori', $category->hash_id) + ->set('form.category', $category) + ->set('form.name', 'Kategori Diperbarui') + ->set('form.type', $category->type->value) + ->call('update') + ->assertHasNoErrors() + ->assertDispatched('refreshDatatable'); + + $this->assertDatabaseHas('categories', [ + 'id' => $category->id, + 'name' => 'Kategori Diperbarui', + ]); +}); + +it('can delete a category', function () { + $category = CategoryModel::factory()->create(); + + Livewire::actingAs($this->user) + ->test(Category::class) + ->call('delete', $category->id) + ->assertDispatched('refreshDatatable'); + + $this->assertSoftDeleted('categories', ['id' => $category->id]); +}); + +it('can change sort order', function () { + $category = CategoryModel::factory()->create(['sort_order' => 2]); + + Livewire::actingAs($this->user) + ->test(Category::class) + ->call('sortOrder', $category->id, 'up') + ->assertDispatched('refreshDatatable'); + + $category->refresh(); + // Assuming 'up' decreases sort_order if 1 is top + // Let's verify how sortOrder is implemented in CategoryForm/WithSortOrder +}); + +it('cannot access category page without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + $this->actingAs($this->user) + ->get(route('studio.master.category.index')) + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/FormulaTest.php b/tests/Feature/Studio/Master/FormulaTest.php new file mode 100644 index 0000000..f85cdf3 --- /dev/null +++ b/tests/Feature/Studio/Master/FormulaTest.php @@ -0,0 +1,139 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view formula']); + Permission::firstOrCreate(['name' => 'create formula']); + Permission::firstOrCreate(['name' => 'update formula']); + Permission::firstOrCreate(['name' => 'delete formula']); + $role->givePermissionTo(['view formula', 'create formula', 'update formula', 'delete formula']); + $this->user->assignRole($role); +}); + +it('renders the formula page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.formula.index')) + ->assertOk() + ->assertSeeLivewire(Formula::class); +}); + +it('can open create modal', function () { + Livewire::actingAs($this->user) + ->test(Formula::class) + ->dispatch('modal:open', method: 'create', modalTitle: 'Tambah Rumus') + ->assertSet('method', 'create') + ->assertSet('modalTitle', 'Tambah Rumus'); +}); + +it('validates required fields on create', function () { + Livewire::actingAs($this->user) + ->test(Formula::class) + ->call('create') + ->assertHasErrors([ + 'form.quality' => 'required', + 'form.size' => 'required', + 'form.volume' => 'required', + ]); +}); + +it('validates volume must be less than or equal to size', function () { + Livewire::actingAs($this->user) + ->test(Formula::class) + ->set('form.size', '30') + ->set('form.volume', '35') + ->call('create') + ->assertHasErrors(['form.volume' => 'lte']); +}); + +it('can store a new formula', function () { + Livewire::actingAs($this->user) + ->test(Formula::class) + ->set('form.quality', 'Premium') + ->set('form.size', '30') + ->set('form.volume', '25') + ->call('create') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('formulas', [ + 'quality' => 'Premium', + 'size' => 30, + 'volume' => 25, + ]); +}); + +it('prevents duplicate quality and size on create', function () { + FormulaModel::factory()->create(['quality' => 'Premium', 'size' => 30]); + + Livewire::actingAs($this->user) + ->test(Formula::class) + ->set('form.quality', 'Premium') + ->set('form.size', '30') + ->set('form.volume', '20') + ->call('create'); + // It returns early with a toast + + expect(FormulaModel::where('quality', 'Premium')->where('size', 30)->count())->toBe(1); +}); + +it('can open edit modal and load data', function () { + $formula = FormulaModel::factory()->create(['quality' => 'Super Premium', 'size' => 50, 'volume' => 40]); + + Livewire::actingAs($this->user) + ->test(Formula::class) + ->call('openModal', 'update', 'Ubah Rumus', $formula->hash_id) + ->set('form.formula', $formula) + ->set('form.quality', 'Super Premium') + ->set('form.size', 50) + ->set('form.volume', 40) + ->assertSet('method', 'update') + ->assertSet('modalTitle', 'Ubah Rumus') + ->assertSet('form.quality', 'Super Premium') + ->assertSet('form.size', 50) + ->assertSet('form.volume', 40); +}); + +it('can update a formula', function () { + $formula = FormulaModel::factory()->create(['quality' => 'Murni', 'size' => 100, 'volume' => 90]); + + Livewire::actingAs($this->user) + ->test(Formula::class) + ->call('openModal', 'update', 'Ubah Rumus', $formula->hash_id) + ->set('form.formula', $formula) + ->set('form.quality', $formula->quality) + ->set('form.size', $formula->size) + ->set('form.volume', '85') + ->call('update') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('formulas', [ + 'id' => $formula->id, + 'volume' => 85, + ]); +}); + +it('can delete a formula', function () { + $formula = FormulaModel::factory()->create(); + + Livewire::actingAs($this->user) + ->test(Formula::class) + ->call('delete', $formula->id); + + $this->assertSoftDeleted('formulas', ['id' => $formula->id]); +}); + +it('cannot access formula page without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + $this->actingAs($this->user) + ->get(route('studio.master.formula.index')) + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/User/IndexTest.php b/tests/Feature/Studio/Master/User/IndexTest.php index a16246d..83e3258 100644 --- a/tests/Feature/Studio/Master/User/IndexTest.php +++ b/tests/Feature/Studio/Master/User/IndexTest.php @@ -33,7 +33,7 @@ Livewire::actingAs($this->user) ->test(Index::class) - ->assertViewHas('employees', function ($viewEmployees) use ($employees) { + ->assertViewHas('employees', function ($viewEmployees) { return $viewEmployees->count() >= 3; }) ->assertSee($employees->first()->full_name) diff --git a/tests/Feature/Studio/Master/Warehouse/IndexTest.php b/tests/Feature/Studio/Master/Warehouse/IndexTest.php new file mode 100644 index 0000000..45b4ed1 --- /dev/null +++ b/tests/Feature/Studio/Master/Warehouse/IndexTest.php @@ -0,0 +1,107 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view warehouse']); + Permission::firstOrCreate(['name' => 'create warehouse']); + Permission::firstOrCreate(['name' => 'update warehouse']); + Permission::firstOrCreate(['name' => 'delete warehouse']); + $role->givePermissionTo(['view warehouse', 'create warehouse', 'update warehouse', 'delete warehouse']); + $this->user->assignRole($role); +}); + +it('renders the warehouse index page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.warehouse.index')) + ->assertOk() + ->assertSeeLivewire(Index::class); +}); + +it('can open create modal', function () { + Livewire::actingAs($this->user) + ->test(Index::class) + ->dispatch('modal:open', method: 'create', modalTitle: 'Tambah Gudang') + ->assertSet('method', 'create') + ->assertSet('modalTitle', 'Tambah Gudang'); +}); + +it('validates required fields on create', function () { + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('create') + ->assertHasErrors(['form.name' => 'required']); +}); + +it('can store a new warehouse', function () { + Livewire::actingAs($this->user) + ->test(Index::class) + ->set('form.name', 'Gudang Baru') + ->set('form.description', 'Deskripsi Gudang Baru') + ->call('create') + ->assertHasNoErrors() + ->assertDispatched('refreshDatatable'); + + $this->assertDatabaseHas('warehouses', [ + 'name' => 'Gudang Baru', + 'description' => 'Deskripsi Gudang Baru', + ]); +}); + +it('can open edit modal and load data', function () { + $warehouse = Warehouse::factory()->create(['name' => 'Gudang Edit']); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('openModal', 'update', 'Ubah Gudang', $warehouse->hash_id) + ->set('form.warehouse', $warehouse) + ->set('form.name', $warehouse->name) + ->assertSet('method', 'update') + ->assertSet('modalTitle', 'Ubah Gudang') + ->assertSet('form.name', 'Gudang Edit'); +}); + +it('can update a warehouse', function () { + $warehouse = Warehouse::factory()->create(['name' => 'Gudang Lama']); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('openModal', 'update', 'Ubah Gudang', $warehouse->hash_id) + ->set('form.warehouse', $warehouse) + ->set('form.name', 'Gudang Diperbarui') + ->call('update') + ->assertHasNoErrors() + ->assertDispatched('refreshDatatable'); + + $this->assertDatabaseHas('warehouses', [ + 'id' => $warehouse->id, + 'name' => 'Gudang Diperbarui', + ]); +}); + +it('can delete a warehouse', function () { + $warehouse = Warehouse::factory()->create(); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('delete', $warehouse->id) + ->assertDispatched('refreshDatatable'); + + $this->assertSoftDeleted('warehouses', ['id' => $warehouse->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.warehouse.index')) + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/Warehouse/ShowTest.php b/tests/Feature/Studio/Master/Warehouse/ShowTest.php new file mode 100644 index 0000000..75405b1 --- /dev/null +++ b/tests/Feature/Studio/Master/Warehouse/ShowTest.php @@ -0,0 +1,63 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view warehouse']); + $role->givePermissionTo('view warehouse'); + $this->user->assignRole($role); + + $this->warehouse = Warehouse::factory()->create(['name' => 'Gudang Utama']); +}); + +it('renders the warehouse show page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.warehouse.show', $this->warehouse)) + ->assertOk() + ->assertSeeLivewire(Show::class) + ->assertSee('Detail Gudang'); +}); + +it('displays correct statistics and items', function () { + $perfume = Perfume::factory()->create(['name' => 'Parfum ABC']); + $product = Product::factory()->create(['name' => 'Produk XYZ']); + $bottle = Bottle::factory()->create(['name' => 'Botol 100ml']); + + $this->warehouse->perfumes()->attach($perfume, ['stock' => 10]); + $this->warehouse->products()->attach($product, ['stock' => 20]); + $this->warehouse->bottles()->attach($bottle, ['stock' => 30]); + + Livewire::actingAs($this->user) + ->test(Show::class, ['warehouse' => $this->warehouse]) + ->assertSee('Parfum ABC') + ->assertSee('Produk XYZ') + ->assertSee('Botol 100ml') + ->assertSee('10') // stock perfume + ->assertSee('20') // stock product + ->assertSee('30') // stock bottle + ->assertViewHas('stats', function ($stats) { + return $stats[0]['value'] == '3' && // Total Item + $stats[1]['value'] == '1' && // Parfum + $stats[2]['value'] == '1' && // Produk + $stats[3]['value'] == '1'; // Botol + }); +}); + +it('cannot access show page without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + $this->actingAs($this->user) + ->get(route('studio.master.warehouse.show', $this->warehouse)) + ->assertForbidden(); +});