feat: Add comprehensive feature tests for master data modules and refine Livewire forms.

This commit is contained in:
Yoga Pangestu 2026-01-02 18:18:13 +07:00
parent 8358e20575
commit 598ca3500f
9 changed files with 470 additions and 95 deletions

View File

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

View File

@ -7,7 +7,7 @@
class WarehouseForm extends Form
{
public ?Warehouse $warehouse;
public ?Warehouse $warehouse = null;
public string $name = '';

View File

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

View File

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

View File

@ -0,0 +1,129 @@
<?php
use App\Enums\CategoryType;
use App\Livewire\Studio\Master\Category;
use App\Models\Category as CategoryModel;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->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();
});

View File

@ -0,0 +1,139 @@
<?php
use App\Livewire\Studio\Master\Formula;
use App\Models\Formula as FormulaModel;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->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();
});

View File

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

View File

@ -0,0 +1,107 @@
<?php
use App\Livewire\Studio\Master\Warehouse\Index;
use App\Models\Warehouse;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->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();
});

View File

@ -0,0 +1,63 @@
<?php
use App\Livewire\Studio\Master\Warehouse\Show;
use App\Models\Bottle;
use App\Models\Perfume;
use App\Models\Product;
use App\Models\Warehouse;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->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();
});